第四版
Fourth Edition
罗彻斯特大学计算机科学系 罗彻斯特大学计算机科学系
Department of Computer Science, University of Rochester Department of Computer Science University of Rochester
1.1 The Art of Language Design
1.2 The Programming Language Spectrum
1.3 Why Study Programming Languages?
1.4 Compilation and Interpretation
1.6 An Overview of Compilation
1.7 Summary and Concluding Remarks
2: Programming Language Syntax
2.1 Specifying Syntax: Regular Expressions and Context-Free Grammars
2.5 Summary and Concluding Remarks
3: Names, Scopes, and Bindings
3.1 The Notion of Binding Time
3.2 Object Lifetime and Storage Management
3.5 The Meaning of Names within a Scope
3.6 The Binding of Referencing Environments
3.9 Summary and Concluding Remarks
4.1 The Role of the Semantic Analyzer
4.5 Space Management for Attributes
4.6 Tree Grammars and Syntax Tree Decoration
4.7 Summary and Concluding Remarks
5: Target Machine Architecture
II: Core Issues in Language Design
II: Core Issues in Language Design
6.2 Structured and Unstructured Flow
6.8 Summary and Concluding Remarks
7.4 Equality Testing and Assignment
7.5 Summary and Concluding Remarks
8.5 Pointers and Recursive Types
8.8 Summary and Concluding Remarks
9: Subroutines and Control Abstraction
9.7 Summary and Concluding Remarks
10: Data Abstraction and Object Orientation
10.1 Object-Oriented Programming
10.2 Encapsulation and Inheritance
10.3 Initialization and Finalization
10.6 True Multiple Inheritance
10.7 Object-Oriented Programming Revisited
10.8 Summary and Concluding Remarks
III: Alternative Programming Models
III: Alternative Programming Models
11.2 Functional Programming Concepts
11.5 Evaluation Order Revisited
11.8 Functional Programming in Perspective
11.9 Summary and Concluding Remarks
12.1 Logic Programming Concepts
12.4 Logic Programming in Perspective
12.5 Summary and Concluding Remarks
13.1 Background and Motivation
13.2 Concurrent Programming Fundamentals
13.3 Implementing Synchronization
13.4 Language-Level Constructs
13.6 Summary and Concluding Remarks
14.1 What Is a Scripting Language?
14.3 Scripting the World Wide Web
14.5 Summary and Concluding Remarks
IV: A Closer Look at Implementation
IV: A Closer Look at Implementation
15: Building a Runnable Program
15.1 Back-End Compiler Structure
15.4 Address Space Organization
15.8 Summary and Concluding Remarks
16: Run-Time Program Management
16.2 Late Binding of Machine Code
A: Programming Languages Mentioned
Morgan Kaufmann 是 Elsevier 旗下品牌
Morgan Kaufmann is an imprint of Elsevier
225 Wyman Street,沃尔瑟姆,马萨诸塞州 02451,美国
225 Wyman Street,Waltham, MA 02451, USA
版权所有 © 2016、2009、2006、1999 Elsevier Inc. 保留所有权利。
Copyright © 2016, 2009, 2006, 1999 Elsevier Inc. All rights reserved.
封面图片:版权所有 © 2009,Shannon A. Scott。
Cover image: Copyright © 2009, Shannon A. Scott.
纽约州那不勒斯的卡明自然中心。
Cumming Nature Center, Naples, NY.
未经出版商书面许可,不得以任何形式或任何方式(电子或机械方式,包括影印、录制或任何信息存储和检索系统)复制或传播本出版物的任何部分。有关如何申请许可的详细信息、有关出版商许可政策的更多信息以及我们与版权许可中心和版权许可机构等组织的安排,请访问我们的网站:www.elsevier.com/permissions。
No part of this publication may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or any information storage and retrieval system, without permission in writing from the publisher. Details on how to seek permission, further information about the Publisher's permissions policies and our arrangements with organizations such as the Copyright Clearance Center and the Copyright Licensing Agency, can be found at our website: www.elsevier.com/permissions.
本书及其所包含的个人贡献均受出版商的版权保护(除非本文另有说明)。
This book and the individual contributions contained in it are protected under copyright by the Publisher (other than as maybe noted herein).
通告
Notices
这一领域的知识和最佳实践在不断变化。随着新的研究和经验拓宽我们的理解,研究方法、专业实践或医疗治疗可能需要改变。
Knowledge and best practice in this field are constantly changing. As new research and experience broaden our understanding, changes in research methods, professional practices, or medical treatment may become necessary.
执业人员和研究人员在评估和使用本文所述的任何信息、方法、化合物或实验时,必须始终依靠自己的经验和知识。在使用此类信息或方法时,他们应注意自身安全和他人的安全,包括他们负有专业责任的人员的安全。
Practitioners and researchers must always rely on their own experience and knowledge in evaluating and using any information, methods, compounds, or experiments described herein. In using such information or methods they should be mindful of their own safety and the safety of others, including parties for whom they have a professional responsibility.
在法律允许的最大范围内,无论是出版商、作者、投稿人或编辑,对于因产品责任、疏忽或其他原因造成的任何人身伤害和/或财产损失,或因使用或操作本文材料中包含的任何方法、产品、说明或想法而造成的任何人身伤害和/或财产损失,均不承担任何责任。
To the fullest extent of the law, neither the Publisher nor the authors, contributors, or editors, assume any liability for any injury and/or damage to persons or property as a matter of products liability, negligence or otherwise, or from any use or operation of any methods, products, instructions, or ideas contained in the material herein.
英国图书馆出版品目錄資料
British Library Cataloguing in Publication Data
大英图书馆提供了该书的目录记录
A catalogue record for this book is available from the British Library
美国国会图书馆出版品目錄數據
Library of Congress Cataloging-in-Publication Data
本书的目录记录可从国会图书馆获取
A catalog record for this book is available from the Library of Congress
有关所有 MK 出版物的信息,请访问我们的网站http://store.elsevier.com/
For information on all MK publications visit our website at http://store.elsevier.com/
国际标准书号:978-0-12-410409-9
ISBN: 978-0-12-410409-9
Michael L. Scott 是罗彻斯特大学计算机科学系的教授,曾任系主任。他于 1985 年获得威斯康星大学麦迪逊分校计算机科学博士学位。2014 年至 2015 年,他担任 Google 客座科学家。他的研究兴趣在于编程语言、操作系统和高级计算机架构的交叉领域,重点是并行和分布式计算。他与 John Mellor-Crummey 共同设计的 MCS 互斥锁被用于各种商业和学术系统。与 Maged Michael、Bill Scherer 和 Doug Lea 共同设计的其他几种算法出现在 java.util.concurrent 标准库中。2006年,他和 Mellor-Crummey 博士共同获得了 ACM SIGACT/SIGOPS Edsger W. Dijkstra 分布式计算奖。
Michael L. Scott is a professor and past chair of the Department of Computer Science at the University of Rochester. He received his Ph.D. in computer sciences in 1985 from the University of Wisconsin–Madison. From 2014–2015 he was a Visiting Scientist at Google. His research interests lie at the intersection of programming languages, operating systems, and high-level computer architecture, with an emphasis on parallel and distributed computing. His MCS mutual exclusion lock, co-designed with John Mellor-Crummey, is used in a variety of commercial and academic systems. Several other algorithms, co-designed with Maged Michael, Bill Scherer, and Doug Lea, appear in the java.util.concurrent standard library. In 2006 he and Dr. Mellor-Crummey shared the ACM SIGACT/SIGOPS Edsger W. Dijkstra Prize in Distributed Computing.
Scott 博士是计算机协会会员、电气电子工程师学会会员,也是 Usenix、忧思科学家联盟和美国大学教授协会的成员。他是 150 多篇经过审查的出版物的作者,曾担任 2003 年 ACM 操作系统原理研讨会 (SOSP) 的总主席,以及 2007 年 ACM SIGPLAN 事务计算研讨会 (TRANSACT)、2008 年 ACM SIGPLAN 并行编程原理与实践研讨会 (PPoPP) 和 2012 年编程语言和操作系统架构支持国际会议 (ASPLOS) 的程序主席。2001 年,他因在本科教学方面的杰出成就和艺术性而获得罗彻斯特大学罗伯特和帕梅拉·戈尔根奖。
Dr. Scott is a Fellow of the Association for Computing Machinery, a Fellow of the Institute of Electrical and Electronics Engineers, and a member of Usenix, the Union of Concerned Scientists, and the American Association of University Professors. The author of more than 150 refereed publications, he served as General Chair of the 2003 ACM Symposium on Operating Systems Principles (SOSP) and as Program Chair of the 2007 ACM SIGPLAN Workshop on Transactional Computing (TRANSACT), the 2008 ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming (PPoPP), and the 2012 International Conference on Architectural Support for Programming Languages and Operating Systems (ASPLOS). In 2001 he received the University of Rochester's Robert and Pamela Goergen Award for Distinguished Achievement and Artistry in Undergraduate Teaching.
致家人和朋友。
To family and friends.
编程语言被普遍认为是每个计算机科学家必须掌握的核心科目之一。原因很明显:这些语言是我们用于开发产品和交流新想法的主要符号。它们通过推动那些塑造信息时代的数百万行程序的开发,影响了该领域。它们的成功归功于计算机科学界长期以来在创建新语言和制定实现策略方面的努力。迈克尔·斯科特在本书的脚注和书目注释中提到的大量计算机科学家以及它所包含的主题的数量和多样性,清楚地体现了这种努力的规模。
Programming languages are universally accepted as one of the core subjects that every computer scientist must master. The reason is clear: these languages are the main notation we use for developing products and for communicating new ideas. They have influenced the field by enabling the development of those multimillion-line programs that shaped the information age. Their success is owed to the long-standing effort of the computer science community in the creation of new languages and in the development of strategies for their implementation. The large number of computer scientists mentioned in the footnotes and bibliographic notes in this book by Michael Scott is a clear manifestation of the magnitude of this effort as is the sheer number and diversity of topics it contains.
本书讨论了超过 75 种编程语言。它们代表了跨时代、跨范式和跨应用领域的语言设计中最佳和最具影响力的贡献。它们是数十年工作的成果,最初在 20 世纪 50 年代诞生了 Fortran 和 Lisp,随后几年又诞生了众多语言,而在我们这个时代,则诞生了用于编写 Web 程序的流行动态语言。这 75 多种语言涵盖了众多范式,包括命令式、函数式、逻辑式、静态式、动态式、顺序式、共享内存并行、分布式内存并行、数据流、高级和中间语言。它们包括用于科学计算、符号操作和访问数据库的语言。语言的丰富多样性对于程序员的工作效率至关重要,也是计算学科的一大财富。
Over 75 programming languages are discussed. They represent the best and most influential contributions in language design across time, paradigms, and application domains. They are the outcome of decades of work that led initially to Fortran and Lisp in the 1950s, to numerous languages in the years that followed, and, in our times, to the popular dynamic languages used to program the Web. The 75 plus languages span numerous paradigms including imperative, functional, logic, static, dynamic, sequential, shared-memory parallel, distributed memory parallel, dataflow, high-level, and intermediate languages. They include languages for scientific computing, for symbolic manipulations, and for accessing databases. This rich diversity of languages is crucial for programmer productivity and is one of the great assets of the discipline of computing.
本书涵盖了各种语言,详细讨论了控制流、类型和抽象机制。这些是开发组织良好、模块化、易于理解和易于维护的程序所需的表示。了解这些核心功能及其在当今语言中的体现是成为一名高效程序员和更好地理解当今计算机科学的基本基础。
Cutting across languages, this book presents a detailed discussion of control flow, types, and abstraction mechanisms. These are the representations needed to develop programs that are well organized, modular, easy to understand, and easy to maintain. Knowledge of these core features and of their incarnation in today's languages is a basic foundation to be an effective programmer and to better understand computer science today.
编程语言的实现策略必须与设计范式一起研究。原因之一是语言的成功取决于其实现的质量。此外,这些策略的能力有时会限制语言的设计。语言的实现始于解析和词汇扫描,这是计算程序句法结构所必需的。第一部分中描述的当今解析技术是有史以来最漂亮的算法之一,也是使用数学对象创建实用工具的一个很好的例子。它们值得研究作为一项智力成就。它们当然具有很大的实用价值,而欣赏这些策略的伟大之处的一个好方法是回到第一个 Fortran 编译器,研究构建该编译器的先驱者用于实现运算符优先级的临时但非常巧妙的策略。
Strategies to implement programming languages must be studied together with the design paradigms. A reason is that success of a language depends on the quality of its implementation. Also, the capabilities of these strategies sometimes constraint the design of languages. The implementation of a language starts with parsing and lexical scanning needed to compute the syntactic structure of programs. Today's parsing techniques, described in Part I, are among the most beautiful algorithms ever developed and are a great example of the use of mathematical objects to create practical instruments. They are worthwhile studying just as an intellectual achievement. They are of course of great practical value, and a good way to appreciate the greatness of these strategies is to go back to the first Fortran compiler and study the ad hoc, albeit highly ingenious, strategy used to implement precedence of operators by the pioneers that built that compiler.
实现的另一个常见组件是编译器组件,它执行从高级语言表示到适合由真实或虚拟机执行的较低级别形式的转换。转换可以提前完成,也可以在执行期间(即时)完成,或者两者兼而有之。本书讨论了这些方法和实现策略,包括由解析驱动的优雅转换机制。为了生成高效的代码,转换例程应用策略来避免冗余计算,有效利用内存层次结构,并利用处理器内并行性。这些有时相互冲突的目标由编译器的优化组件承担。虽然这个主题通常超出了编译器第一门课程的范围,但本书让读者可以在第 IV 部分中很好地了解程序优化。
The other usual component of implementation are the compiler components that carry out the translation from the high-level language representation to a lower level form suitable for execution by real or virtual machines. The translation can be done ahead of time, during execution (just in time), or both. The book discusses these approaches and implementation strategies including the elegant mechanisms of translation driven by parsing. To produce highly efficient code, translation routines apply strategies to avoid redundant computations, make efficient use of the memory hierarchy, and take advantage ofintra-processor parallelism. These, sometimes conflicting goals, are undertaken by the optimization components of compilers. Although this topic is typically outside the scope of a first course on compilers, the book gives the reader access to a good overview of program optimization in Part IV.
计算领域的一个重要最新发展是并行性的普及,人们预计在可预见的未来,性能提升将主要来自有效利用这种并行性。本书通过向读者介绍并发编程中的一系列主题(包括线程间的同步、通信和协调机制)来响应这一发展。随着并行性逐渐成为计算领域的常态,这些信息将变得越来越重要。
An important recent development in computing is the popularization of parallelism and the expectation that, in the foreseeable future, performance gains will mainly be the result of effectively exploiting this parallelism. The book responds to this development by presenting the reader with a range of topics in concurrent programming including mechanisms for synchronization, communication, and coordination across threads. This information will become increasingly important as parallelism consolidates as the norm in computing.
编程语言是程序员和机器之间的桥梁。算法必须在编程语言中表示出来才能执行。编程语言设计和实现的研究需要理解用于连接计算不同方面的策略,因此具有很大的教育价值。Michael Scott 的《编程语言语用学》对这一主题进行了如此广泛的论述,是对文献的一大贡献,也是计算机科学家的宝贵信息来源。
Programming languages are the bridge between programmers and machines. It is in them that algorithms must be represented for execution. The study of programming languages design and implementation offers great educational value by requiring an understanding of the strategies used to connect the different aspects of computing. By presenting such an extensive treatment of the subject, Michael Scott’s Programming Language Pragmatics, is a great contribution to the literature and a valuable source of information for computer scientists.
计算机编程课程是普通学生首次接触计算机科学领域的课程。大多数选修该课程的学生一生都在使用计算机,用于社交网络、电子邮件、游戏、网页浏览、文字处理和许多其他任务,但直到他们编写了第一个程序后,他们才开始了解应用程序的工作原理。在获得一定的程序员能力后(大概是在数据结构和算法课程的帮助下),自然而然的下一步就是想知道编程语言是如何工作的。本书对此进行了解释。它的目标很简单,就是成为最全面、最准确的语言教材,风格引人入胜,普通本科生可以理解。这一目标反映了我的信念:如果我们解释真正发生的事情,学生将更好地理解,并更享受这些材料。
A course in computer programming provides the typical student's first exposure to the field of computer science. Most students in such a course will have used computers all their lives, for social networking, email, games, web browsing, word processing, and a host of other tasks, but it is not until they write their first programs that they begin to appreciate how applications work. After gaining a certain level of facility as programmers (presumably with the help of a good course in data structures and algorithms), the natural next step is to wonder how programming languages work. This book provides an explanation. It aims, quite simply, to be the most comprehensive and accurate languages text available, in a style that is engaging and accessible to the typical undergraduate. This aim reflects my conviction that students will understand more, and enjoy the material more, if we explain what is really going on.
在传统的“系统”课程中,数据结构(可能还有计算机组织)以外的内容往往被划分为许多单独的科目,包括编程语言、编译器构造、计算机体系结构、操作系统、网络、并行和分布式计算、数据库管理系统,以及可能的软件工程、面向对象设计、图形或用户界面系统。这种划分的一个问题是,科目列表不断增加,但学士学位课程的学期数却没有增加。也许更重要的是,计算机科学中许多最有趣的发现都发生在学科之间的边界上。例如,计算机体系结构和编译器构造在 50 多年来一直相互启发,经历了一代又一代的超级计算机、流水线微处理器、多核芯片和现代 GPU。在过去十年中,虚拟化技术的进步模糊了硬件、操作系统、编译器和语言运行时系统之间的界限,并刺激了云计算的爆炸式增长。编程语言技术现在已常规嵌入从动态 Web 内容到游戏和娱乐、再到安全和金融等各个领域。
In the conventional “systems” curriculum, the material beyond data structures (and possibly computer organization) tends to be compartmentalized into a host of separate subjects, including programming languages, compiler construction, computer architecture, operating systems, networks, parallel and distributed computing, database management systems, and possibly software engineering, object-oriented design, graphics, or user interface systems. One problem with this compartmentalization is that the list of subjects keeps growing, but the number of semesters in a Bachelor's program does not. More important, perhaps, many of the most interesting discoveries in computer science occur at the boundaries between subjects. Computer architecture and compiler construction, for example, have inspired each other for over 50 years, through generations of supercomputers, pipelined microprocessors, multicore chips, and modern GPUs. Over the past decade, advances in virtualization have blurred boundaries among the hardware, operating system, compiler, and language run-time system, and have spurred the explosion in cloud computing. Programming language technology is now routinely embedded in everything from dynamic web content, to gaming and entertainment, to security and finance.
教育工作者和从业者都越来越重视这种互动。尤其是在高等教育中,核心课程的整合趋势日益明显。许多学校不再让普通学生深入学习两三门狭窄的科目,而在其他科目中留有空白,而是修改了编程语言和计算机组织课程,以涵盖更广泛的主题,并在课程中增加后续选修课各种专业。这一趋势与 ACM/IEEE-CS计算机科学课程 2013指南 [ SR13 ]非常吻合,该指南强调需要管理课程规模,并培养“系统级视角”和对理论与实践之间相互作用的理解。作者特别写道,
Increasingly, both educators and practitioners have come to emphasize these sorts of interactions. Within higher education in particular, there is a growing trend toward integration in the core curriculum. Rather than give the typical student an in-depth look at two or three narrow subjects, leaving holes in all the others, many schools have revised the programming languages and computer organization courses to cover a wider range of topics, with follow-on electives in various specializations. This trend is very much in keeping with the ACM/IEEE-CS Computer Science Curricula 2013 guidelines [SR13], which emphasize the need to manage the size of the curriculum and to cultivate both a “system-level perspective” and an appreciation of the interplay between theory and practice. In particular, the authors write,
计算机科学专业的毕业生需要在多个细节和抽象层面上进行思考。这种理解应该超越各个组件的实现细节,涵盖对计算机系统结构及其构建和分析过程的理解 [p. 24]。
Graduates of a computer science program need to think at multiple levels of detail and abstraction. This understanding should transcend the implementation details of the various components to encompass an appreciation for the structure of computer systems and the processes involved in their construction and analysis [p. 24].
关于这篇文章的具体主题,他们写道
On the specific subject of this text, they write
编程语言是程序员精确描述概念、制定算法和推理解决方案的媒介。在职业生涯中,计算机科学家将使用多种不同的语言,无论是单独使用还是一起使用。软件开发人员必须了解不同语言背后的编程模型,并在支持多种互补方法的语言中做出明智的设计选择。计算机科学家经常需要学习新的语言和编程结构,并且必须了解编程语言特性的定义、组成和实现背后的原理。有效使用编程语言并了解其局限性还需要具备编程语言翻译和静态程序分析的基本知识,以及内存管理等运行时组件 [p. 155]。
Programming languages are the medium through which programmers precisely describe concepts, formulate algorithms, and reason about solutions. In the course of a career, a computer scientist will work with many different languages, separately or together. Software developers must understand the programming models underlying different languages and make informed design choices in languages supporting multiple complementary approaches. Computer scientists will often need to learn new languages and programming constructs, and must understand the principles underlying how programming language features are defined, composed, and implemented. The effective use of programming languages, and appreciation of their limitations, also requires a basic knowledge of programming language translation and static program analysis, as well as run-time components such as memory management [p. 155].
《编程语言语用学》 (PLP)的前三版有幸顺应了综合理解的潮流。第四版延续并强化了“系统视角”,同时仍将重点放在编程语言设计上。
The first three editions of Programming Language Pragmatics (PLP) had the good fortune of riding the trend toward integrated understanding. This fourth edition continues and strengthens the “systems perspective” while preserving the central focus on programming language design.
从本质上讲,PLP 是一本关于编程语言工作原理的书。它没有列举许多不同语言的细节,而是专注于学生可能遇到的所有语言的基本概念,用各种具体的例子来说明这些概念,并探索解释不同语言为何以不同方式设计的权衡。同样,PLP 也没有解释如何构建编译器或解释器(很少有程序员会完全承担这项任务),而是专注于编译器对输入程序的作用及其原因。因此,语言设计和实现是一起探讨的,重点是它们之间的相互作用方式。
At its core, PLP is a book about how programming languages work. Rather than enumerate the details of many different languages, it focuses on concepts that underlie all the languages the student is likely to encounter, illustrating those concepts with a variety of concrete examples, and exploring the tradeoffs that explain why different languages were designed in different ways. Similarly, rather than explain how to build a compiler or interpreter (a task few programmers will undertake in its entirety), PLP focuses on what a compiler does to an input program, and why. Language design and implementation are thus explored together, with an emphasis on the ways in which they interact.
与第三版相比,PLP-4e 包括
In comparison to the third edition, PLP-4e includes
1. 新增了关于类型系统和复合类型的章节,取代了之前关于类型的单独章节
1. New chapters devoted to type systems and composite types, in place of the older single chapter on types
2. 更新了函数式编程的处理方式,并广泛涵盖了 OCaml
2. Updated treatment of functional programming, with extensive coverage of OCaml
3. 该领域变化的许多其他反映
3. Numerous other reflections of changes in the field
4. 受讲师反馈或对熟悉主题的重新思考启发而做出的改进
4. Improvements inspired by instructor feedback or a fresh consideration of familiar topics
此列表中的第 1 项可能是最明显的变化。第 7 章是以前版本中最长的,主题材料自然而然地被拆分。PLP-4e 重新组织了这些材料,让我们有机会更加明确地关注类型推断的主题,尤其是它在 ML 系列语言中的作用。它还促进了参数多态性材料的更新和重新组织,这些材料以前分散在几个不同的章节中。
Item 1 in this list is perhaps the most visible change. Chapter 7 was the longest in previous editions, and there is a natural split in the subject material. Reorganization of this material for PLP-4e afforded an opportunity to devote more explicit attention to the subject of type inference, and of its role in ML-family languages in particular. It also facilitated an update and reorganization of the material on parametric polymorphism, which was previously scattered across several different chapters and sections.
第 2 项反映了函数式技术在主流命令式语言中的日益普及,以及 SML、OCaml 和 Haskell 在教育和工业领域的日益突出地位。在整篇文章中,OCaml 现在与 Scheme 一样,都是函数式编程示例的来源。如上一段所述,ML 类型系统有一个扩展部分 (7.2.4) ,第 11.4 节包括 OCaml 概述,涵盖了相等和排序、绑定和 lambda 表达式、类型构造函数、模式匹配以及控制流和副作用。选择 OCaml 而不是 Haskell 作为 ML 系列示例反映了其在工业领域的突出地位,同时课堂经验表明——至少对于许多学生来说——在急切求值的背景下,最初接触函数式思维会更容易。对于那些希望我选择 Haskell 的同事,我深表歉意!
Item 2 reflects the increasing adoption of functional techniques into mainstream imperative languages, as well as the increasing prominence of SML, OCaml, and Haskell in both education and industry. Throughout the text, OCaml is now co-equal with Scheme as a source of functional programming examples. As noted in the previous paragraph, there is an expanded section (7.2.4) on the ML type system, and Section 11.4 includes an OCaml overview, with coverage of equality and ordering, bindings and lambda expressions, type constructors, pattern matching, and control flow and side effects. The choice of OCaml, rather than Haskell, as the ML-family exemplar reflects its prominence in industry, together with classroom experience suggesting that—at least for many students—the initial exposure to functional thinking is easier in the context of eager evaluation. To colleagues who wish I'd chosen Haskell, my apologies!
其他新材料(第 3 项)出现在整篇文章中。在适当的地方,都引用了最新语言和标准的特性,包括 C 和 C++11、Java 8、C# 5、Scala、Go、Swift、Python 3 和 HTML 5。第 3.6.4 节汇集了之前分散的 lambda 表达式介绍,并展示了如何将它们添加到各种命令式语言中。第 10.4.4 节补充了对象闭包,包括 C++11 的std::function和std::bind。第 c-5.4.5 节介绍了 x86-64 和 ARM 体系结构,以取代以前版本中使用的 x86-32 和 MIPS。使用这两种相同体系结构的示例随后出现在调用序列(9.2)和链接(15.6)部分。x86 调用序列的介绍继续依赖于gcc;ARM 案例研究使用 LLVM。第 8.5.3 节介绍了智能指针。第 9.3.1 节中出现了 R 值引用。第 9.6.2 节的图形示例中,JavaFX 取代了 Swing 。附录 A中新增了 Go、Lua、Rust、Scala 和 Swift 的条目。
Other new material (Item 3) appears throughout the text. Wherever appropriate, reference has been made to features of the latest languages and standards, including C & C++11, Java 8, C# 5, Scala, Go, Swift, Python 3, and HTML 5. Section 3.6.4 pulls together previously scattered coverage of lambda expressions, and shows how these have been added to various imperative languages. Complementary coverage of object closures, including C++11's std::function and std::bind, appears in Section 10.4.4. Section c-5.4.5 introduces the x86-64 and ARM architectures in place of the x86-32 and MIPS used in previous editions. Examples using these same two architectures subsequently appear in the sections on calling sequences (9.2) and linking (15.6). Coverage of the x86 calling sequence continues to rely on gcc; the ARM case study uses LLVM. Section 8.5.3 introduces smart pointers. R-value references appear in Section 9.3.1. JavaFX replaces Swing in the graphics examples of Section 9.6.2. Appendix A has new entries for Go, Lua, Rust, Scala, and Swift.
最后,第 4 项包含对文本几乎每个部分的改进。更新较多的主题包括 FOLLOW 和 PREDICT 集(第 2.3.3 节);Wirth 的递归下降错误恢复算法(第 c-2.3.5 节);重载(第 3.5.2 节);模块(第 3.3.4 节);鸭子类型(第 7.3 节);记录和变体(第 8.1 节);侵入式列表(从第 10 章的运行示例中删除);静态字段和方法(第 10.2.2 节);混合继承(从配套站点移回正文,并更新以涵盖 Scala 特征和 Java 8 默认方法);多核处理器(第 13 章的普遍变化);移相器(第 13.3.1 节);内存模型(第 13.3.3 节);信号量(第 13.3.5 节);未来(第 13.4.5 节);GIMPLE 和 RTL(第 c-15.2.1节);QEMU(第 16.2.2 节);DWARF(第 16.3.2 节);以及语言谱系(图 A.1)。
Finally, Item 4 encompasses improvements to almost every section of the text. Among the more heavily updated topics are FOLLOW and PREDICT sets (Section 2.3.3); Wirth's error recovery algorithm for recursive descent (Section c-2.3.5); overloading (Section 3.5.2); modules (Section 3.3.4); duck typing (Section 7.3); records and variants (Section 8.1); intrusive lists (removed from the running example of Chapter 10); static fields and methods (Section 10.2.2); mix-in inheritance (moved from the companion site back into the main text, and updated to cover Scala traits and Java 8 default methods); multicore processors (pervasive changes to Chapter 13); phasers (Section 13.3.1); memory models (Section 13.3.3); semaphores (Section 13.3.5); futures (Section 13.4.5); GIMPLE and RTL (Section c-15.2.1); QEMU (Section 16.2.2); DWARF (Section 16.3.2); and language genealogy (Figure A.1).
为了容纳新材料,一些主题的覆盖范围已被压缩甚至删除。例如模块(第 3 章和第 10 章)、变体记录和 with 语句(第 8 章)以及元循环解释(第 11 章)。附加材料(尤其是公共语言基础结构 (CLI))已移至配套站点。在整篇文章中,来自不再广泛使用的语言的示例已在适当的情况下用更新的等效语言替换。几乎所有剩余的对 Pascal 和 Modula 的引用都只是历史性的。对 Occam 和 Tcl 的大部分介绍也已被删除。
To accommodate new material, coverage of some topics has been condensed or even removed. Examples include modules (Chapters 3 and 10), variant records and with statements (Chapter 8), and metacircular interpretation (Chapter 11). Additional material—the Common Language Infrastructure (CLI) in particular—has moved to the companion site. Throughout the text, examples drawn from languages no longer in widespread use have been replaced with more recent equivalents wherever appropriate. Almost all remaining references to Pascal and Modula are merely historical. Most coverage of Occam and Tcl has also been dropped.
总体而言,印刷文本增加了约 40 页。增加了 5 个“设计与实现”边栏、35 个编号示例以及约 25 个新的章末练习和探索。我们投入了大量精力来创建一致且全面的索引。与之前的版本一样,Morgan Kaufmann 一直致力于以合理的价格提供权威文本:PLP-4e 比竞争替代品便宜得多,但内容更丰富、更全面。
Overall, the printed text has grown by roughly 40 pages. There are 5 more “Design & Implementation” sidebars, 35 more numbered examples, and about 25 new end-of-chapter exercises and explorations. Considerable effort has been invested in creating a consistent and comprehensive index. As in earlier editions, Morgan Kaufmann has maintained its commitment to providing definitive texts at reasonable cost: PLP-4e is far less expensive than competing alternatives, but larger and more comprehensive.
为了尽量减少文本的物理尺寸,为新材料腾出空间,并让学生在浏览时专注于基础知识,可以在配套网站上找到超过 350 页的更高级或外围材料:booksite.elsevier.com/web/9780124104099。每个配套网站 (CS) 部分在正文中都由主题的简短介绍和总结省略材料的“更深入”段落表示。
To minimize the physical size of the text, make way for new material, and allow students to focus on the fundamentals when browsing, over 350 pages of more advanced or peripheral material can be found on a companion web site: booksite.elsevier.com/web/9780124104099. Each companion-site (CS) section is represented in the main text by a brief introduction to the subject and an “In More Depth” paragraph that summarizes the elided material.
请注意,将材料放在配套网站上并不构成对其技术重要性的判断。它只是反映了一个事实,即值得介绍的材料比一卷书或一学期课程所能容纳的要多。由于偏好和教学大纲各不相同,大多数教师可能希望从 CS 中指定阅读材料,并且大多数教师不会指定印刷文本的某些部分。我的目的是保留印刷版中可能在最多课程中涵盖的材料。
Note that placement of material on the companion site does not constitute a judgment about its technical importance. It simply reflects the fact that there is more material worth covering than will fit in a single volume or a single-semester course. Since preferences and syllabi vary, most instructors will probably want to assign reading from the CS, and most will refrain from assigning certain sections of the printed text. My intent has been to retain in print the material that is likely to be covered in the largest number of courses.
CS 中还包括指向在线资源的指针和文本中所有重要代码片段的可编译副本(超过二十种语言)。
Also included on the CS are pointers to on-line resources and compilable copies of all significant code fragments found in the text (in more than two dozen languages).
与其前身一样,PLP-4e 非常重视语言设计对实现选项的限制,以及预期实现对语言设计的影响。其中许多联系和相互作用在 140 个“设计与实现”边栏中进行了重点介绍。边栏 1.1 中有更详细的介绍。附录 B中有编号列表。
Like its predecessors, PLP-4e places heavy emphasis on the ways in which language design constrains implementation options, and the ways in which anticipated implementations have influenced language design. Many of these connections and interactions are highlighted in some 140 “Design & Implementation” sidebars. A more detailed introduction appears in Sidebar 1.1. A numbered list appears in Appendix B.
PLP-4e 中的示例与演示流程紧密结合。为了便于查找特定示例、记住其内容并在其他上下文中引用它们,每个示例的编号和标题都显示在边注中。正文和 CS 中有 1000 多个此类示例。详细列表见附录C。
Examples in PLP-4e are intimately woven into the flow of the presentation. To make it easier to find specific examples, to remember their content, and to refer to them in other contexts, a number and a title for each is displayed in a marginal note. There are over 1000 such examples across the main text and the CS. A detailed list appears in Appendix C.
复习题在正文中每隔大约 10 页出现一次,位于主要章节的末尾。这些复习题直接基于前面的内容,并有简短、直接的答案。
Review questions appear throughout the text at roughly 10-page intervals, at the ends of major sections. These are based directly on the preceding material, and have short, straightforward answers.
每章末尾都有更详细的问题。这些问题分为练习和探索。前者通常比每节复习问题更具挑战性,适合家庭作业或简短的项目。后者更开放,需要网络或图书馆研究、大量时间投入或形成主观意见。注册教师可以从受密码保护的网站获取许多练习(但不包括探索)的解决方案:请访问textbooks.elsevier.com/web/9780124104099。
More detailed questions appear at the end of each chapter. These are divided into Exercises and Explorations. The former are generally more challenging than the per-section review questions, and should be suitable for homework or brief projects. The latter are more open-ended, requiring web or library research, substantial time commitment, or the development of subjective opinion. Solutions to many of the exercises (but not the explorations) are available to registered instructors from a password-protected web site: visit textbooks.elsevier.com/web/9780124104099.
《编程语言语用学》涵盖了计算机课程 2013报告 [ SR13 ]中 PL“知识单元”的几乎所有材料。本书的设计对象是罗彻斯特大学的语言课程,该课程实际上是报告中的特色“课程范例”之一(第 369-371 页)。图 1说明了文本中的几种可能路径。
Programming Language Pragmatics covers almost all of the material in the PL “knowledge units” of the Computing Curricula 2013 report [SR13]. The languages course at the University of Rochester, for which this book was designed, is in fact one of the featured “course exemplars” in the report (pp. 369–371). Figure 1illustrates several possible paths through the text.
对于自学或全年课程(图 1中的F轨道),我建议从头到尾学习本书,在遇到每个“更深入”部分时转向配套网站。罗切斯特的一学期课程(R轨道)也涵盖了本书的大部分内容,但遗漏了大部分 CS章节,以及自下而上的解析(2.3.4)、逻辑语言(第 12 章)和第 15 章(构建可运行程序)和第 16 章(运行时程序管理)的后半部分。请注意,函数式编程(特别是第 11 章)的材料可以用 OCaml 或 Scheme 来教授。
For self-study, or for a full-year course (track F in Figure 1), I recommend working through the book from start to finish, turning to the companion site as each “In More Depth” section is encountered. The one-semester course at Rochester (track R) also covers most of the book, but leaves out most of the CS sections, as well as bottom-up parsing (2.3.4), logic languages (Chapter 12), and the second halves of Chapters 15 (Building a Runnable Program) and 16 (Runtime Program Management). Note that the material on functional programming (Chapter 11 in particular) can be taught in either OCaml or Scheme.
某些章节(2、4、5、15、16、17 )比其他章节更侧重于实现问题。这些章节可以相对于更面向设计的章节进行一定程度的重新排序。许多学生已经熟悉第 5 章中的大部分材料,很可能来自计算机组织课程;因此将本章放在配套网站上。一些学生可能还熟悉第2 章中的部分材料,也许来自自动机理论课程。本章的大部分内容也可以快速阅读,也许停下来思考一些实际问题,如从语法错误中恢复,或者扫描器与经典有限自动机的不同之处。
Some chapters (2, 4, 5, 15,16, 17) have a heavier emphasis than others on implementation issues. These can be reordered to a certain extent with respect to the more design-oriented chapters. Many students will already be familiar with much of the material in Chapter 5, most likely from a course on computer organization; hence the placement of the chapter on the companion site. Some students may also be familiar with some of the material in Chapter 2, perhaps from a course on automata theory. Much of this chapter can then be read quickly as well, pausing perhaps to dwell on such practical issues as recovery from syntax errors, or the ways in which a scanner differs from a classical finite automaton.
传统的编程语言课程(图 1中的P轨道)可能会省略所有扫描和解析,以及第 4 章的全部内容。它还会从头到尾淡化面向实现的材料。取而代之的是,它可以添加面向设计的 CS 部分,如多重继承(10.6)、Smalltalk(10.7.1)、lambda 演算(11.7)和谓词演算(12.3)。
A traditional programming languages course (track P in Figure 1) might leave out all of scanning and parsing, plus all of Chapter 4. It would also de-emphasize the more implementation-oriented material throughout. In place of these, it could add such design-oriented CS sections as multiple inheritance (10.6), Smalltalk (10.7.1), lambda calculus (11.7), and predicate calculus (12.3).
一些学校还使用 PLP 作为编译器入门课程(图 1中的C部分)。典型的教学大纲省略了第 III 部分(第 11 章至第 14 章)的大部分内容,并且从头到尾都淡化了更注重设计的材料。取而代之的是,它包括了所有扫描和解析内容、第 15 章至第 17章,以及略有不同的其他 CS 部分。
PLP has also been used at some schools for an introductory compiler course (track C in Figure 1). The typical syllabus leaves out most of Part III (Chapters 11 through 14), and de-emphasizes the more design-oriented material throughout. In place of these, it includes all of scanning and parsing, Chapters 15 through 17, and a slightly different mix of other CS sections.
对于采用学季制的学校,一个有吸引力的选择是提供一门为期一个学季的入门课程和两门可选的后续课程(图 1中的Q轨道)。入门学季可能涵盖第 1、3、6、7 和 8 章的主要(非 CS)部分,以及第2和9 章的前半部分。面向语言的后续学季可能涵盖第 9 章的其余部分、全部第 III 部分、第 6 至第 9 章的 CS 部分,以及可能的形式语义、类型理论或其他相关主题的补充材料。面向编译器的后续学季可能涵盖第 2 章的其余部分;第 4至第 5 章和第15至第 17章、第 3和第 9至第10章的 CS 部分,以及可能关于自动代码生成、积极代码改进、编程工具等的补充材料。
For a school on the quarter system, an appealing option is to offer an introductory one-quarter course and two optional follow-on courses (track Q in Figure 1). The introductory quarter might cover the main (non-CS) sections of Chapters 1, 3, 6, 7, and 8, plus the first halves of Chapters 2 and 9. A language-oriented follow-on quarter might cover the rest of Chapter 9, all of Part III, CS sections from Chapters 6 through 9, and possibly supplemental material on formal semantics, type theory, or other related topics. A compiler-oriented follow-on quarter might cover the rest of Chapter 2; Chapters 4–5 and 15–17, CS sections from Chapters 3 and 9–10, and possibly supplemental material on automatic code generation, aggressive code improvement, programming tools, and so on.
无论以何种方式阅读本书,我都假定读者已经对至少一种命令式语言有了丰富的使用经验。具体是哪种语言并不重要。书中的示例来自多种语言,但都附有足够的注释和其他讨论,即使没有相关经验的读者也应该能够轻松理解。附录 A中提供了 60 多种不同语言的单段介绍。如果需要,算法将以非正式的伪代码形式呈现,这些伪代码应该是不言自明的。真正的编程语言代码采用“打字机”字体。伪代码采用无衬线字体。
Whatever the path through the text, I assume that the typical reader has already acquired significant experience with at least one imperative language. Exactly which language it is shouldn't matter. Examples are drawn from a wide variety of languages, but always with enough comments and other discussion that readers without prior experience should be able to understand easily. Single-paragraph introductions to more than 60 different languages appear in Appendix A. Algorithms, when needed, are presented in an informal pseudocode that should be self-explanatory. Real programming language code is set in “typewriter“ font. Pseudocode is set in a sans-serif font.
除了补充章节外,配套网站还包含所有非平凡示例的完整源代码,以及书中所有已知错误的列表。其他资源可在线获取,网址为textbooks.elsevier.com/web/9780124104099。对于采用该文本的教师,密码保护页面可访问
In addition to supplemental sections, the companion site contains complete source code for all nontrivial examples, and a list of all known errors in the book. Additional resources are available on-line at textbooks.elsevier.com/web/9780124104099. For instructors who have adopted the text, a password-protected page provides access to
■ Editable PDF source for all the figures in the book
■ 可编辑的 PowerPoint 幻灯片
■ Editable PowerPoint slides
■ 大部分练习的答案
■ Solutions to most of the exercises
■ 对大型项目的建议
■ Suggestions for larger projects
在编写第四版的过程中,我得到了很多人的慷慨帮助。许多人对第三版提供了勘误表或其他反馈,其中包括 Yacine Belkadi、Björn Brandenburg、Bob Cochran、Daniel Crisman、Marcelino Debajo、Chen Ding、Peter Drake、Michael Edgar、Michael Glass、Sergio Gomes、Allan Gottlieb、Hossein Hadavi、Chris Hart、Thomas Helmuth、Wayne Heym、Scott Hoge、Kelly Jones、Ahmed Khademzadeh、Eleazar Enrique Leal、Kyle Liddell、Annie Liu、Hao Luo、Dirk Müller、Holger Peine、Andreas Priesnitz、Mikhail Prokharau、Harsh Raju 和 Jingguo Yao。我还非常感谢在之前版本中感谢的许多人,以及使这些版本取得成功的评论者、采用者和读者。
In preparing the fourth edition, I have been blessed with the generous assistance of a very large number of people. Many provided errata or other feedback on the third edition, among them Yacine Belkadi, Björn Brandenburg, Bob Cochran, Daniel Crisman, Marcelino Debajo, Chen Ding, Peter Drake, Michael Edgar, Michael Glass, Sergio Gomes, Allan Gottlieb, Hossein Hadavi, Chris Hart, Thomas Helmuth, Wayne Heym, Scott Hoge, Kelly Jones, Ahmed Khademzadeh, Eleazar Enrique Leal, Kyle Liddell, Annie Liu, Hao Luo, Dirk Müller, Holger Peine, Andreas Priesnitz, Mikhail Prokharau, Harsh Raju, and Jingguo Yao. I also remain indebted to the many individuals acknowledged in previous editions, and to the reviewers, adopters, and readers who made those editions a success.
第四版的匿名审阅者提供了大量有用的建议;我向你们所有人表示感谢!特别感谢麻省理工学院的 Adam Chlipala 对函数式编程内容的详细而深刻的建议。我还要感谢 Nelson Beebe(犹他大学),他指出编译器不能安全地对可能是 NaN 的浮点数使用整数比较;感谢 Dan Scarafoni 提示我在生成 PREDICT 集的算法中区分符号的 FIRST/EPS 和字符串的 FIRST/EPS;感谢 Dave Musicant 建议改进深度绑定的描述;感谢 Allan Gottlieb(纽约大学)对 Ada 语义的几个关键澄清;感谢 Benjamin Kowarsch 对 Objective-C 的类似澄清。所有这些领域中仍然存在的问题完全是我自己的问题。
Anonymous reviewers for the fourth edition provided a wealth of useful suggestions; my thanks to all of you! Special thanks to Adam Chlipala of MIT for his detailed and insightful suggestions on the coverage of functional programming. My thanks as well to Nelson Beebe (University of Utah) for pointing out that compilers cannot safely use integer comparisons for floating-point numbers that may be NaNs; to Dan Scarafoni for prompting me to distinguish between FIRST/EPS of symbols and FIRST/EPS of strings in the algorithm to generate PREDICT sets; to Dave Musicant for suggested improvements to the description of deep binding; to Allan Gottlieb (NYU) for several key clarifications regarding Ada semantics; and to Benjamin Kowarsch for similar clarifications regarding Objective-C. Problems that remain in all these areas are entirely my own.
在编写第四版时,我借鉴了在罗彻斯特大学向高年级本科生教授此教材的 25 年经验。我感谢所有学生的热情和反馈。我还要感谢我的同事和研究生,以及系里的行政、秘书和技术人员,感谢他们提供了如此支持和高效的工作环境。最后,我要感谢 David Padua,自从读研究生以来,我一直很钦佩他的作品;我很荣幸他能成为序言的作者。
In preparing the fourth edition, I have drawn on 25 years of experience teaching this material to upper-level undergraduates at the University of Rochester. I am grateful to all my students for their enthusiasm and feedback. My thanks as well to my colleagues and graduate students, and to the department's administrative, secretarial, and technical staff for providing such a supportive and productive work environment. Finally, my thanks to David Padua, whose work I have admired since I was in graduate school; I am deeply honored to have him as the author of the Foreword.
正如之前的版本一样,与 Morgan Kaufmann 的员工合作是一种真正的乐趣,无论是在专业层面还是个人层面。我特别感谢高级开发编辑 Nate McFadden,他以无尽的耐心、幽默感和对细节的敏锐眼光指导了这一版和前两版;感谢管理本书制作的 Mohana Natarajan;感谢出版商 Todd Green,他在更大的 Elsevier 世界中保持了 Morgan Kauffman 印记的个人风格。
As they were on previous editions, the staff at Morgan Kaufmann has been a genuine pleasure to work with, on both a professional and a personal level. My thanks in particular to Nate McFadden, Senior Development Editor, who shepherded both this and the previous two editions with unfailing patience, good humor, and a fine eye for detail; to Mohana Natarajan, who managed the book's production; and to Todd Green, Publisher, who upholds the personal touch of the Morgan Kauffman imprint within the larger Elsevier universe.
最重要的是,我要感谢我的妻子凯利,感谢她在我写作和修改的漫长岁月中给予我的耐心和支持。计算机是一项很好的职业,但家庭才是最重要的。
Most important, I am indebted to my wife, Kelly, for her patience and support through endless months of writing and revising. Computing is a fine profession, but family is what really matters.
基础
Foundations
编程语言语用学的一个核心前提是语言设计和实现密切相关;很难只研究其中一个而不研究另一个。
A central premise of Programming Language Pragmatics is that language design and implementation are intimately connected; it's hard to study one without the other.
本书的大部分内容(第二部分和第三部分)是围绕语言设计的主题展开的,但详细介绍了设计决策如何受到实现问题的影响的多种方式。
The bulk of the text—Parts II and III—is organized around topics in language design, but with detailed coverage throughout of the many ways in which design decisions have been shaped by implementation concerns.
前五章(第一部分)通过介绍设计和实现方面的基础材料奠定了基础。第 1 章激发了对编程语言的研究,介绍了主要的语言系列,并概述了编译过程。第 3 章介绍了程序的高级结构,重点介绍了名称、名称与对象的绑定以及控制在任何给定时间哪些绑定处于活动状态的范围规则。在此过程中,它涉及存储管理;子例程、模块和类;多态性;以及单独编译。
The first five chapters—Part I—set the stage by covering foundational material in both design and implementation. Chapter 1 motivates the study of programming languages, introduces the major language families, and provides an overview of the compilation process. Chapter 3 covers the high-level structure of programs, with an emphasis on names, the binding of names to objects, and the scope rules that govern which bindings are active at any given time. In the process it touches on storage management; subroutines, modules, and classes; polymorphism; and separate compilation.
第 2、4 和 5 章更侧重于实现。它们提供了理解第 II 和第 III 部分中提到的实现问题所需的背景知识。第2 章讨论了程序的语法或文本结构。它介绍了设计人员用来描述程序语法的正则表达式和上下文无关文法,以及编译器或解释器用来识别该语法的扫描和解析算法。在理解了语法之后,第 4 章解释了编译器(或解释器)如何确定程序的语义或含义。讨论围绕属性文法的概念展开,属性文法用于将程序映射到其他有意义的东西上,例如数学或其他现有语言。最后,第 5 章(完全在配套网站上)概述了汇编级计算机体系结构,重点介绍了与编译器最相关的现代微处理器的功能。了解这些功能的程序员不仅更有可能理解他们使用的语言为什么是这样设计的,而且还能尽可能充分有效地使用这些语言。
Chapters 2, 4, and 5 are more implementation oriented. They provide the background needed to understand the implementation issues mentioned in Parts II and III. Chapter 2 discusses the syntax, or textual structure, of programs. It introduces regular expressions and context-free grammars, which designers use to describe program syntax, together with the scanning and parsing algorithms that a compiler or interpreter uses to recognize that syntax. Given an understanding of syntax, Chapter 4 explains how a compiler (or interpreter) determines the semantics, or meaning of a program. The discussion is organized around the notion of attribute grammars, which serve to map a program onto something else that has meaning, such as mathematics or some other existing language. Finally, Chapter 5 (entirely on the companion site) provides an overview of assembly-level computer architecture, focusing on the features of modern microprocessors most relevant to compilers. Programmers who understand these features have a better chance not only of understanding why the languages they use were designed the way they were, but also of using those languages as fully and effectively as possible.
汇编语言最初设计时就将助记符与机器语言指令一一对应,如本例所示。1将助记符转换为机器语言成为了系统程序(称为汇编程序)的工作。汇编程序最终增加了精巧的“宏扩展”功能,允许程序员为常用指令序列定义参数化缩写。然而,汇编语言和机器语言之间的对应关系仍然显而易见且明确。编程仍然是一项以机器为中心的事业:每种不同类型的计算机都必须用自己的汇编语言进行编程,程序员则以机器实际执行的指令来思考。
Assembly languages were originally designed with a one-to-one correspondence between mnemonics and machine language instructions, as shown in this example.1 Translating from mnemonics to machine language became the job of a systems program known as an assembler. Assemblers were eventually augmented with elaborate “macro expansion” facilities to permit programmers to define parameterized abbreviations for common sequences of instructions. The correspondence between assembly language and machine language remained obvious and explicit, however. Programming continued to be a machine-centered enterprise: each different kind of computer had to be programmed in its own assembly language, and programmers thought in terms of the instructions that the machine would actually execute.
随着计算机的发展和竞争性设计的出现,为每台新机器重写程序变得越来越令人沮丧。人类也越来越难以跟踪大型汇编语言程序中的大量细节。人们开始希望有一种独立于机器的语言,特别是一种数值计算(当时最常见的程序类型)可以用更接近数学公式的东西来表达的语言。这些愿望导致了 20 世纪 50 年代中期 Fortran 的原始方言的开发,这是第一个可以说是高级编程语言的语言。其他高级语言也很快出现,尤其是 Lisp 和 Algol。
As computers evolved, and as competing designs developed, it became increasingly frustrating to have to rewrite programs for every new machine. It also became increasingly difficult for human beings to keep track of the wealth of detail in large assembly language programs. People began to wish for a machine-independent language, particularly one in which numerical computations (the most common type of program in those days) could be expressed in something more closely resembling mathematical formulae. These wishes led in the mid-1950s to the development of the original dialect of Fortran, the first arguably high-level programming language. Other high-level languages soon followed, notably Lisp and Algol.
将高级语言翻译成汇编语言或机器语言是系统程序(称为编译器)的工作。2编译器比汇编程序复杂得多,因为当源是高级语言时,源操作和目标操作之间的一一对应关系不再存在。 Fortran 最初流行起来很慢,因为人类程序员只要付出一些努力,几乎总是可以编写比编译器运行速度更快的汇编语言程序。然而,随着时间的推移,性能差距已经缩小,并最终逆转。硬件复杂性的增加(由于流水线、多个功能单元等)和编译器技术的不断改进导致了这样一种情况,即最先进的编译器通常会生成比人类更好的代码。即使在人类可以做得更好的情况下,计算机速度和程序大小的增加也使得节省程序员的努力变得越来越重要,不仅在程序的原始构造中,而且在后续程序维护——增强和修正。劳动力成本现在远远超过计算硬件的成本。
Translating from a high-level language to assembly or machine language is the job of a systems program known as a compiler.2 Compilers are substantially more complicated than assemblers because the one-to-one correspondence between source and target operations no longer exists when the source is a high-level language. Fortran was slow to catch on at first, because human programmers, with some effort, could almost always write assembly language programs that would run faster than what a compiler could produce. Over time, however, the performance gap has narrowed, and eventually reversed. Increases in hardware complexity (due to pipelining, multiple functional units, etc.) and continuing improvements in compiler technology have led to a situation in which a state-of-the-art compiler will usually generate better code than a human being will. Even in cases in which human beings can do better, increases in computer speed and program size have made it increasingly important to economize on programmer effort, not only in the original construction of programs, but in subsequent program maintenance—enhancement and correction. Labor costs now heavily outweigh the cost of computing hardware.
如今,高级编程语言有数千种,而且新的语言还在不断涌现。为什么会有这么多呢?有几种可能的答案:
Today there are thousands of high-level programming languages, and new ones continue to emerge. Why are there so many? There are several possible answers:
演化。计算机科学是一门年轻的学科;我们一直在寻找更好的方法来做事。20 世纪 60 年代末和 70 年代初,“结构化编程”发生了一场革命,Fortran、Cobol 和 Basic 3等语言的基于goto的控制流让位于while循环、case (switch)语句和类似的高级结构。20 世纪 80 年代末,Algol、Pascal 和 Ada 等语言的嵌套块结构开始让位于 Smalltalk、C++、Eiffel 等语言的面向对象结构,以及十年后的 Java 和 C#。最近,Python 和 Ruby 等脚本语言开始取代更传统的编译语言,至少在快速开发方面是如此。
Evolution. Computer science is a young discipline; we're constantly finding better ways to do things. The late 1960s and early 1970s saw a revolution in “structured programming,” in which the goto-based control flow of languages like Fortran, Cobol, and Basic3 gave way to while loops, case (switch) statements, and similar higher-level constructs. In the late 1980s the nested block structure of languages like Algol, Pascal, and Ada began to give way to the object-oriented structure of languages like Smalltalk, C++, Eiffel, and—a decade later—Java and C#. More recently, scripting languages like Python and Ruby have begun to displace more traditional compiled languages, at least for rapid development.
特殊用途。有些语言是为特定问题领域设计的。各种 Lisp 方言适合处理符号数据和复杂数据结构。Icon 和 Awk 适合处理字符串。C 适合低级系统编程。Prolog 适合推理数据之间的逻辑关系。这些语言中的每一种都可以成功地用于更广泛的任务,但重点显然在于专业性。
Special Purposes. Some languages were designed for a specific problem domain. The various Lisp dialects are good for manipulating symbolic data and complex data structures. Icon and Awk are good for manipulating character strings. C is good for low-level systems programming. Prolog is good for reasoning about logical relationships among data. Each of these languages can be used successfully for a wider range of tasks, but the emphasis is clearly on the specialty.
个人偏好。不同的人喜欢不同的东西。编程的狭隘性很大程度上只是个人品味的问题。有些人喜欢 C 的简洁性,有些人讨厌它。有些人觉得递归思考很自然,而另一些人则喜欢迭代。有些人喜欢使用指针,而另一些人则喜欢 Lisp、Java 和 ML 的隐式解引用。个人偏好的强烈和多样性使得不可能有人能开发出一种普遍接受的编程语言。
Personal Preference. Different people like different things. Much of the parochialism of programming is simply a matter of taste. Some people love the terseness of C; some hate it. Some people find it natural to think recursively; others prefer iteration. Some people like to work with pointers; others prefer the implicit dereferencing of Lisp, Java, and ML. The strength and variety of personal preference make it unlikely that anyone will ever develop a universally acceptable programming language.
当然,有些语言比其他语言更成功。在众多已设计的语言中,只有几十种被广泛使用。什么使一种语言成功?同样,有几个答案:
Of course, some languages are more successful than others. Of the many that have been designed, only a few dozen are widely used. What makes a language successful? Again there are several answers:
表达能力。人们经常听到一种语言比另一种语言更“强大”的说法,尽管从形式数学意义上讲,它们都是图灵完备— 每种语言都可以用来实现任意算法,尽管用起来有些别扭。不过,语言特性显然对程序员编写清晰、简洁、可维护的代码的能力有着巨大的影响,尤其是对于非常大的系统。例如,早期版本的 Basic 和 C++ 之间没有可比性。本书主要关注的是那些有助于表达能力的因素 — 尤其是抽象功能。
Expressive Power. One commonly hears arguments that one language is more “powerful” than another, though in a formal mathematical sense they are all Turing complete—each can be used, if awkwardly, to implement arbitrary algorithms. Still, language features clearly have a huge impact on the programmer's ability to write clear, concise, and maintainable code, especially for very large systems. There is no comparison, for example, between early versions of Basic on the one hand, and C++ on the other. The factors that contribute to expressive power—abstraction facilities in particular—are a major focus of this book.
新手易用。虽然 Basic 很容易上手,但不可否认它的成功。成功的一部分原因在于其“学习曲线”非常低。多年来,入门级编程语言课程都教授 Pascal,因为至少与其他“严肃”语言相比,它紧凑且易于学习。世纪之交后不久,Java 开始扮演类似的角色;尽管它比 Pascal 复杂得多,但比 C++ 等语言更简单。为了重新追求简单性,近年来一些入门课程已转向 Python 等脚本语言。
Ease of Use for the Novice. While it is easy to pick on Basic, one cannot deny its success. Part of that success was due to its very low “learning curve.” Pascal was taught for many years in introductory programming language courses because, at least in comparison to other “serious” languages, it was compact and easy to learn. Shortly after the turn of the century, Java came to play a similar role; though substantially more complex than Pascal, it is simpler than, say, C++. In a renewed quest for simplicity, some introductory courses in recent years have turned to scripting languages like Python.
易于实现。除了学习难度低之外,Basic 的成功还在于它能够在资源有限的微型机器上轻松实现。出于类似的原因,Forth 拥有一小群忠实的追随者。可以说,Pascal 成功最重要的一个因素是其设计者 Niklaus Wirth 开发了一种简单、可移植的语言实现,并将其免费提供给世界各地的大学(参见示例 1.15)。4 Java和 Python 的设计者采取了类似的措施,让几乎所有需要的人都可以免费使用他们的语言。
Ease of Implementation. In addition to its low learning curve, Basic was successful because it could be implemented easily on tiny machines, with limited resources. Forth had a small but dedicated following for similar reasons. Arguably the single most important factor in the success of Pascal was that its designer, Niklaus Wirth, developed a simple, portable implementation of the language, and shipped it free to universities all over the world (see Example 1.15).4 The Java and Python designers took similar steps to make their language available for free to almost anyone who wants it.
标准化。几乎每种广泛使用的语言都有一个官方的国际标准或(对于几种脚本语言而言)一个单一的规范实现;在后一种情况下,规范实现几乎总是用具有标准的语言编写的。标准化(包括语言和大量库的标准化)是确保代码跨平台可移植性的唯一真正有效的方法。Pascal 的标准相对贫乏,缺少许多程序员认为必不可少的几个功能(单独编译、字符串、静态初始化、随机访问 I/O),这至少是该语言在 20 世纪 80 年代失宠的部分原因。其中许多功能由不同的供应商以不同的方式实现。
Standardization. Almost every widely used language has an official international standard or (in the case of several scripting languages) a single canonical implementation; and in the latter case the canonical implementation is almost invariably written in a language that has a standard. Standardization—of both the language and a broad set of libraries—is the only truly effective way to ensure the portability of code across platforms. The relatively impoverished standard for Pascal, which was missing several features considered essential by many programmers (separate compilation, strings, static initialization, random-access I/O), was at least partially responsible for the language's drop from favor in the 1980s. Many of these features were implemented in different ways by different vendors.
开源。当今大多数编程语言都至少有一个开源编译器或解释器,但有些语言(尤其是 C 语言)与其他语言相比,与自由分发、同行评审、社区支持的计算联系更为密切。C 语言最初是在 20 世纪早期开发的。20 世纪 70 年代,贝尔实验室的 Dennis Ritchie 和 Ken Thompson 开发了 C 语言,5与最初的 Unix 操作系统的设计相结合。多年来,Unix 逐渐发展成为世界上最具可移植性的操作系统——学术计算机科学的首选操作系统——而 C 语言与它密切相关。随着 C 语言的标准化,该语言开始在各种各样的其他平台上使用。领先的开源操作系统 Linux 就是用 C 语言编写的。截至 2015 年 6 月,C 语言及其后代占据了各种与语言相关的在线内容的一半以上,包括网页参考、图书销售、招聘信息和开源存储库更新。
Open Source. Most programming languages today have at least one open-source compiler or interpreter, but some languages—C in particular—are much more closely associated than others with freely distributed, peer-reviewed, community-supported computing. C was originally developed in the early 1970s by Dennis Ritchie and Ken Thompson at Bell Labs,5 in conjunction with the design of the original Unix operating system. Over the years Unix evolved into the world's most portable operating system—the OS of choice for academic computer science—and C was closely associated with it. With the standardization of C, the language became available on an enormous variety of additional platforms. Linux, the leading open-source operating system, is written in C. As of June 2015, C and its descendants account for well over half of a variety oflanguage-related on-line content, including web page references, book sales, employment listings, and open-source repository updates.
优秀的编译器。Fortran 的成功很大程度上要归功于极其优秀的编译器。这在一定程度上是历史的偶然。Fortran 的存在时间比其他任何语言都要长,各大公司都投入了大量的时间和金钱来开发能够生成非常快的代码的编译器。然而,这也是一个语言设计的问题:Fortran 90 之前的 Fortran 方言缺少递归和指针,这些功能大大增加了生成快速代码的任务的复杂度(至少对于那些没有它们也能以合理的方式编写的程序而言!)。同样,一些语言(例如 Common Lisp)之所以成功,部分原因是它们的编译器和支持工具能够出色地帮助程序员管理非常大的项目。
Excellent Compilers. Fortran owes much of its success to extremely good compilers. In part this is a matter of historical accident. Fortran has been around longer than anything else, and companies have invested huge amounts of time and money in making compilers that generate very fast code. It is also a matter of language design, however: Fortran dialects prior to Fortran 90 lacked recursion and pointers, features that greatly complicate the task of generating fast code (at least for programs that can be written in a reasonable fashion without them!). In a similar vein, some languages (e.g., Common Lisp) have been successful in part because they have compilers and supporting tools that do an unusually good job of helping the programmer manage very large projects.
经济、赞助和惯性。最后,除了技术优势之外,还有其他因素对成功有很大影响。强大赞助商的支持就是其中之一。PL/I 至少在初步估计中,其生命力归功于 IBM。Cobol 和 Ada 的生命力归功于美国国防部。C# 的生命力归功于微软。近年来,Objective-C 作为 iPhone 和 iPad 应用程序的官方语言,人气飙升。在生命周期的另一端,一些语言在出现“更好”的替代方案后仍被广泛使用,因为有大量已安装的软件和程序员专业知识,而替换这些语言的成本太高。例如,世界上许多金融基础设施仍然主要在 Cobol 中运行。
Economics, Patronage, and Inertia. Finally, there are factors other than technical merit that greatly influence success. The backing of a powerful sponsor is one. PL/I, at least to first approximation, owed its life to IBM. Cobol and Ada owe their life to the U. S. Department of Defense. C# owes its life to Microsoft. In recent years, Objective-C has enjoyed an enormous surge in popularity as the official language for iPhone and iPad apps. At the other end of the life cycle, some languages remain widely used long after “better” alternatives are available, because of a huge base of installed software and programmer expertise, which would cost too much to replace. Much of the world's financial infrastructure, for example, still functions primarily in Cobol.
显然,没有哪个单一因素可以决定一种语言是否“好”。在研究编程语言时,我们需要从多个角度考虑问题。特别是,我们需要考虑程序员和语言实现者的观点。有时这些观点会一致,比如对执行速度的渴望。然而,通常会出现冲突和权衡,因为功能的概念吸引力与其实现成本相平衡。当实现不仅对使用该功能的程序施加成本,而且对不使用此功能的程序也施加成本时,权衡就变得特别棘手。
Clearly no single factor determines whether a language is “good.” As we study programming languages, we shall need to consider issues from several points of view. In particular, we shall need to consider the viewpoints of both the programmer and the language implementor. Sometimes these points of view will be in harmony, as in the desire for execution speed. Often, however, there will be conflicts and tradeoffs, as the conceptual appeal of a feature is balanced against the cost of its implementation. The tradeoff becomes particularly thorny when the implementation imposes costs not only on programs that use the feature, but also on programs that do not.
在计算发展的早期,实现者的观点占主导地位。编程语言逐渐发展成为一种告诉计算机该做什么的手段。然而,对于程序员来说,语言更恰当的定义是表达算法的一种手段。正如自然语言限制了阐述和论述一样,编程语言也限制了什么可以表达,什么不能轻易表达,并且对程序员的想法有着深刻而微妙的影响。Donald Knuth 建议将编程视为告诉另一个人你希望计算机做什么的艺术 [ Knu84 ]6。这个定义也许是最好的妥协。它承认概念清晰度和实现效率都是基本关注点。本书试图通过同时考虑其涉及的每个主题的概念和实现方面来体现这种妥协精神。
In the early days of computing the implementor's viewpoint was predominant. Programming languages evolved as a means of telling a computer what to do. For programmers, however, a language is more aptly defined as a means of expressing algorithms. Just as natural languages constrain exposition and discourse, so programming languages constrain what can and cannot easily be expressed, and have both profound and subtle influence over what the programmer can think. Donald Knuth has suggested that programming be regarded as the art of telling another human being what one wants the computer to do [Knu84].6 This definition perhaps strikes the best sort of compromise. It acknowledges that both conceptual clarity and implementation efficiency are fundamental concerns. This book attempts to capture this spirit of compromise, by simultaneously considering the conceptual and implementation aspects of each of the topics it covers.
声明式语言在某种意义上是“更高级的”;它们更符合程序员的观点,而不太符合实现者的观点。然而,命令式语言占主导地位,主要是出于性能原因。在声明式语言的设计中,存在着一种矛盾,一方面希望摆脱“不相关的”实现细节,另一方面又需要足够接近细节以至少控制算法的轮廓。毕竟,高效算法的设计是计算机科学的大部分内容。目前尚不清楚在多大程度上以及在哪些问题领域中,我们可以期望编译器为非常高抽象层次上陈述的问题找到好的算法。在编译器无法找到好算法的任何领域中,程序员都需要能够明确指定一个算法。
Declarative languages are in some sense “higher level”; they are more in tune with the programmer's point of view, and less with the implementor's point of view. Imperative languages predominate, however, mainly for performance reasons. There is a tension in the design of declarative languages between the desire to get away from “irrelevant” implementation details and the need to remain close enough to the details to at least control the outline of an algorithm. The design of efficient algorithms, after all, is what much of computer science is about. It is not yet clear to what extent, and in what problem domains, we can expect compilers to discover good algorithms for problems stated at a very high level of abstraction. In any domain in which the compiler cannot find a good algorithm, the programmer needs to be able to specify one explicitly.
在声明式和命令式语法家族中,有几个重要的子家族:
Within the declarative and imperative families, there are several important subfamilies:
■ 函数式语言采用基于函数递归定义的计算模型。它们的灵感来自lambda 演算,这是 Alonzo Church 在 20 世纪 30 年代开发的一种正式计算模型。本质上,程序被视为从输入到输出的函数,通过细化过程以更简单的函数来定义。此类别的语言包括 Lisp、ML 和 Haskell。
■ Functional languages employ a computational model based on the recursive definition of functions. They take their inspiration from the lambda calculus, a formal computational model developed by Alonzo Church in the 1930s. In essence, a program is considered a function from inputs to outputs, defined in terms of simpler functions through a process of refinement. Languages in this category include Lisp, ML, and Haskell.
■ 数据流语言将计算建模为原始功能节点之间的信息流(令牌) 。它们提供了一种固有的并行模型:节点由输入令牌的到达触发,并且可以并发操作。Id 和 Val 是数据流语言的示例。Sisal 是 Val 的后代,通常被描述为一种函数式语言。
■ Dataflow languages model computation as the flow of information (tokens) among primitive functional nodes. They provide an inherently parallel model: nodes are triggered by the arrival of input tokens, and can operate concurrently. Id and Val are examples of dataflow languages. Sisal, a descendant of Val, is more often described as a functional language.
■ 逻辑或基于约束的语言从谓词逻辑中汲取灵感。它们将计算建模为尝试查找满足某些指定关系的值,使用目标导向搜索通过一系列逻辑规则。Prolog 是最著名的逻辑语言。该术语有时也适用于 SQL 数据库语言、XSLT 脚本语言以及电子表格(如 Excel 及其前身)的可编程方面。
■ Logic or constraint-based languages take their inspiration from predicate logic. They model computation as an attempt to find values that satisfy certain specified relationships, using goal-directed search through a list of logical rules. Prolog is the best-known logic language. The term is also sometimes applied to the SQL database language, the XSLT scripting language, and programmable aspects of spreadsheets such as Excel and its predecessors.
■ 冯·诺依曼语言可能是最常见和应用最广泛的语言。它们包括 Fortran、Ada、C 以及所有其他以修改变量为基本计算手段的语言。7函数式语言基于具有值的表达式,而冯·诺依曼语言则基于语句(尤其是赋值语句),这些语句通过改变内存值的副作用来影响后续计算。
■ The von Neumann languages are probably the most familiar and widely used. They include Fortran, Ada, C, and all of the others in which the basic means of computation is the modification of variables.7 Whereas functional languages are based on expressions that have values, von Neumann languages are based on statements (assignments in particular) that influence subsequent computation via the side effect of changing the value of memory.
■ 面向对象语言的根源可以追溯到 Simula 67。大多数面向对象语言与冯·诺依曼语言密切相关,但具有更结构化和分布式的内存和计算模型。面向对象语言不是将计算描绘成单片处理器对单片内存的操作,而是将其描绘成半独立对象之间的交互,每个对象都有自己的内部状态和用于管理该状态的子例程。Smalltalk 是最纯粹的面向对象语言;C++ 和 Java 可能是使用最广泛的语言。也可以设计面向对象的函数式语言(其中最著名的是 CLOS [ Kee89 ] 和 OCaml),但它们往往具有强烈的命令式风格。
■ Object-oriented languages trace their roots to Simula 67. Most are closely related to the von Neumann languages, but have a much more structured and distributed model of both memory and computation. Rather than picture computation as the operation of a monolithic processor on a monolithic memory, object-oriented languages picture it as interactions among semi-independent objects, each of which has both its own internal state and subroutines to manage that state. Smalltalk is the purest of the object-oriented languages; C++ and Java are probably the most widely used. It is also possible to devise object-oriented functional languages (the best known of these are CLOS [Kee89] and OCaml), but they tend to have a strong imperative flavor.
■ 脚本语言的特点是,它们强调协调或“粘合”来自周围环境的组件。一些脚本语言最初是为特定目的而开发的:csh和bash是作业控制 (shell) 程序的输入语言;PHP 和 JavaScript 主要用于生成动态 Web 内容;Lua 被广泛用于控制计算机游戏。其他语言,包括 Perl、Python 和 Ruby,则更注重通用性。大多数语言都强调快速原型设计,更注重表达的简易性而不是执行速度。
■ Scripting languages are distinguished by their emphasis on coordinating or “gluing together” components drawn from some surrounding context. Several scripting languages were originally developed for specific purposes: csh and bash are the input languages of job control (shell) programs; PHP and JavaScript are primarily intended for the generation of dynamic web content; Lua is widely used to control computer games. Other languages, including Perl, Python, and Ruby, are more deliberately general purpose. Most place an emphasis on rapid prototyping, with a bias toward ease of expression over speed of execution.
有人可能会认为并发(并行)语言应该形成一个单独的家族(本书确实用一章来介绍此类语言),但并发和顺序执行之间的区别与上述分类基本无关。目前,大多数并发程序都是使用特殊库包或编译器结合 Fortran 或 C 等顺序语言编写的。一些广泛使用的语言(包括 Java、C# 和 Ada)具有明确的并发特性。研究人员正在研究此处提到的每个语言家族中的并发性。
One might suspect that concurrent (parallel) languages would form a separate family (and indeed this book devotes a chapter to such languages), but the distinction between concurrent and sequential execution is mostly independent of the classifications above. Most concurrent programs are currently written using special library packages or compilers in conjunction with a sequential language such as Fortran or C. A few widely used languages, including Java, C#, and Ada, have explicitly concurrent features. Researchers are investigating concurrency in each of the language families mentioned here.
需要强调的是,语言家族之间的区别并不明显。例如,冯·诺依曼语言和面向对象语言之间的区别通常非常模糊,而且许多脚本语言也是面向对象的。大多数函数式和逻辑语言都包含一些命令式特性,而最近几种命令式语言也增加了函数式特性。上述描述旨在捕捉各家族的一般特征,而不是提供正式定义。
It should be emphasized that the distinctions among language families are not clear-cut. The division between the von Neumann and object-oriented languages, for example, is often very fuzzy, and many scripting languages are also object-oriented. Most of the functional and logic languages include some imperative features, and several recent imperative languages have added functional features. The descriptions above are meant to capture the general flavor of the families, without providing formal definitions.
本书主要关注命令式语言(冯·诺依曼语言和面向对象语言)。然而,许多问题都跨越了不同的范畴,感兴趣的读者会发现本书大多数章节中都有很多适用于替代计算模型的内容。第11 章至第 14章包含有关函数式、逻辑、并发和脚本语言的附加材料。
Imperative languages—von Neumann and object-oriented—receive the bulk of the attention in this book. Many issues cut across family lines, however, and the interested reader will discover much that is applicable to alternative computational models in most chapters of the book. Chapters 11 through 14 contain additional material on functional, logic, concurrent, and scripting languages.
编程语言是计算机科学和典型计算机科学课程的核心。与大多数车主一样,熟悉一种或多种高级语言的学生通常对学习其他语言以及了解“引擎盖下”发生的事情感到好奇。学习语言很有趣。它也很实用。
Programming languages are central to computer science, and to the typical computer science curriculum. Like most car owners, students who have become familiar with one or more high-level languages are generally curious to learn about other languages, and to know what is going on “under the hood.” Learning about languages is interesting. It's also practical.
首先,对语言设计和实现的良好理解可以帮助人们为任何给定的任务选择最合适的语言。大多数语言在某些方面比其他方面更胜一筹。很少有程序员会选择 Fortran 进行符号计算或字符串处理,但其他选择却远没有那么明确。对于系统编程,应该选择 C、C++ 还是 C#?对于科学计算,应该选择 Fortran 还是 C?对于基于 Web 的应用程序,应该选择 PHP 还是 Ruby?对于嵌入式系统,应该选择 Ada 还是 C?对于图形用户界面,应该选择 Visual Basic 还是 Java?本书应该可以帮助您做出这样的决定。
For one thing, a good understanding of language design and implementation can help one choose the most appropriate language for any given task. Most languages are better for some things than for others. Few programmers are likely to choose Fortran for symbolic computing or string processing, but other choices are not nearly so clear-cut. Should one choose C, C++, or C# for systems programming? Fortran or C for scientific computations? PHP or Ruby for a web-based application? Ada or C for embedded systems? Visual Basic or Java for a graphical user interface? This book should help equip you to make such decisions.
同样,本书应该使学习新语言变得更容易。许多语言是密切相关的。如果你已经了解 C++,那么 Java 和 C# 就更容易学习;如果你已经了解 Scheme,那么 Common Lisp 就更容易学习;如果你已经了解 ML,那么 Haskell 就更容易学习。更重要的是,所有编程语言都有一些基本概念。这些概念中的大多数都是本书各章的主题:类型、控制(迭代、选择、递归、非确定性、并发)、抽象和命名。用这些概念来思考,可以更容易地吸收新语言的语法(形式)和语义(含义),而不是凭空而来。这种情况类似于自然界中发生的情况语言:熟悉语法形式可以更轻松地学习外语。
Similarly, this book should make it easier to learn new languages. Many languages are closely related. Java and C# are easier to learn if you already know C++; Common Lisp if you already know Scheme; Haskell if you already know ML. More importantly, there are basic concepts that underlie all programming languages. Most of these concepts are the subject of chapters in this book: types, control (iteration, selection, recursion, nondeterminacy, concurrency), abstraction, and naming. Thinking in terms of these concepts makes it easier to assimilate the syntax (form) and semantics (meaning) of new languages, compared to picking them up in a vacuum. The situation is analogous to what happens in natural languages: a good knowledge of grammatical forms makes it easier to learn a foreign language.
无论您学习什么语言,了解其设计和实现中的决策都可以帮助您更好地使用它。本书应该可以帮助您:
Whatever language you learn, understanding the decisions that went into its design and implementation will help you use it better. This book should help you:
理解晦涩难懂的功能。典型的 C++ 程序员很少使用联合、多重继承、可变数量的参数或 .* 运算符。(如果您不知道这些是什么,请不要担心!)就像它简化了新语言的吸收一样,理解基本概念可以让您在手册中查找详细信息时更容易理解这些功能。
Understand obscure features. The typical C++ programmer rarely uses unions, multiple inheritance, variable numbers of arguments, or the .* operator. (If you don't know what these are, don't worry!) Just as it simplifies the assimilation of new languages, an understanding of basic concepts makes it easier to understand these features when you look up the details in the manual.
根据对实现成本的了解,选择表达事物的备选方法。例如,在 C++ 中,程序员可能需要避免不必要的临时变量,并尽可能使用复制构造函数,以尽量减少初始化成本。在 Java 中,他们可能希望使用Executor对象,而不是显式创建线程。对于某些(较差的)编译器,他们可能需要采用特殊的编程习惯用法来获得最快的代码:用于数组遍历的指针;x*x 而不是 x**2。在任何语言中,他们都需要能够评估抽象的备选实现之间的权衡——例如,对于位集基数等函数,在计算和表查找之间进行权衡,这些函数可以用任何一种方式实现。
Choose among alternative ways to express things, based on a knowledge of implementation costs. In C++, for example, programmers may need to avoid unnecessary temporary variables, and use copy constructors whenever possible, to minimize the cost of initialization. In Java they may wish to use Executor objects rather than explicit thread creation. With certain (poor) compilers, they may need to adopt special programming idioms to get the fastest code: pointers for array traversal; x*x instead of x**2. In any language, they need to be able to evaluate the tradeoffs among alternative implementations of abstractions—for example between computation and table lookup for functions like bit set cardinality, which can be implemented either way.
充分利用调试器、汇编器、链接器和相关工具。一般来说,高级语言程序员不需要为实现细节而烦恼。但有时,了解这些细节几乎是必不可少的。如果一个人愿意深入了解这些细节,那么顽固的错误或不寻常的系统构建问题可能会变得容易得多。
Make good use of debuggers, assemblers, linkers, and related tools. In general, the high-level language programmer should not need to bother with implementation details. There are times, however, when an understanding of those details is virtually essential. The tenacious bug or unusual system-building problem may be dramatically easier to handle if one is willing to peek at the bits.
模拟缺少有用功能的语言。某些非常有用的功能在较旧的语言中缺失,但可以通过遵循刻意的(如果不是强制的)编程风格来模拟。例如,在 Fortran 的较旧方言中,熟悉现代控制结构的程序员可以使用注释和自律来编写结构良好的代码。同样,在抽象功能较差的语言中,注释和命名约定可以帮助模仿模块化结构,并且可以使用子例程和静态变量来模仿Clu、C#、Python 和 Ruby 中极其有用的迭代器(我们将在第 6.5.3 节中学习)。
Simulate useful features in languages that lack them. Certain very useful features are missing in older languages, but can be emulated by following a deliberate (if unenforced) programming style. In older dialects of Fortran, for example, programmers familiar with modern control constructs can use comments and self-discipline to write well-structured code. Similarly, in languages with poor abstraction facilities, comments and naming conventions can help imitate modular structure, and the extremely useful iterators of Clu, C#, Python, and Ruby (which we will study in Section 6.5.3) can be imitated with subroutines and static variables.
无论语言技术出现在哪里,都要更好地利用它。大多数程序员永远不会设计或实现传统的编程语言,但大多数程序员需要语言技术来完成其他编程任务。典型的个人计算机包含数十种结构化格式的文件,包括文字处理、电子表格、演示文稿、光栅和矢量图形、音乐、视频、数据库以及各种其他应用领域。Web 内容越来越多地以 XML 表示,这是一种基于文本的格式,旨在通过 XSLT 脚本语言轻松操作(在C-14.3.5 节中讨论)。要解析、分析、生成、优化和其他操作的代码因此,几乎任何复杂的程序都可以找到操纵结构化数据的方法,所有这些代码都基于语言技术。掌握了这项技术的程序员将更有能力编写结构良好、可维护的工具。
Make better use of language technology wherever it appears. Most programmers will never design or implement a conventional programming language, but most will need language technology for other programming tasks. The typical personal computer contains files in dozens of structured formats, encompassing word processing, spreadsheets, presentations, raster and vector graphics, music, video, databases, and a wide variety of other application domains. Web content is increasingly represented in XML, a text-based format designed for easy manipulation in the XSLT scripting language (discussed in Section C-14.3.5). Code to parse, analyze, generate, optimize, and otherwise manipulate structured data can thus be found in almost any sophisticated program, and all of this code is based on language technology. Programmers with a strong grasp of this technology will be in a better position to write well-structured, maintainable tools.
类似地,大多数工具本身可以通过启动配置文件、命令行参数、输入命令或内置扩展语言(将在第 14 章中详细讨论)进行定制。我的主目录中有 250 多个单独的配置(“首选项”)文件。我个人的emacs文本编辑器配置文件包含 1200 多行 Lisp 代码。如今,几乎任何复杂程序的用户都需要充分利用配置或扩展语言。这种程序的设计者要么需要采用(并调整)某些现有的扩展语言,要么发明自己的新符号。精通语言理论的程序员将能够更好地设计出优雅、结构良好的符号,以满足当前用户的需求并促进未来的开发。
In a similar vein, most tools themselves can be customized, via start-up configuration files, command-line arguments, input commands, or built-in extension languages (considered in more detail in Chapter 14). My home directory holds more than 250 separate configuration (“preference”) files. My personal configuration files for the emacs text editor comprise more than 1200 lines of Lisp code. The user of almost any sophisticated program today will need to make good use of configuration or extension languages. The designers of such a program will need either to adopt (and adapt) some existing extension language, or to invent new notation of their own. Programmers with a strong grasp of language theory will be in a better position to design elegant, well-structured notation that meets the needs of current users and facilitates future development.
最后,如果您愿意,本书应该可以帮助您为进一步研究语言设计或实现做好准备。如果您对这些领域感兴趣,它还将帮助您理解语言与操作系统和体系结构之间的交互。
Finally, this book should help prepare you for further study in language design or implementation, should you be so inclined. It will also equip you to understand the interactions of languages with operating systems and architectures, should those areas draw your interest.
一般而言,解释比编译具有更大的灵活性和更好的诊断(错误消息)。由于直接执行源代码,解释器可以包含出色的源代码级调试器。它还可以处理那些程序基本特性(例如变量的大小和类型,甚至哪些名称引用哪些变量)取决于输入数据的语言。某些语言特性几乎不可能在没有解释的情况下实现:例如,在 Lisp 和 Prolog 中,程序可以编写自己的新片段并动态执行它们。(几种脚本语言也提供此功能。)将程序实现的决策推迟到运行时称为后期绑定;我们将在第 3.1 节中详细讨论它。
In general, interpretation leads to greater flexibility and better diagnostics (error messages) than does compilation. Because the source code is being executed directly, the interpreter can include an excellent source-level debugger. It can also cope with languages in which fundamental characteristics of the program, such as the sizes and types of variables, or even which names refer to which variables, can depend on the input data. Some language features are almost impossible to implement without interpretation: in Lisp and Prolog, for example, a program can write new pieces of itself and execute them on the fly. (Several scripting languages also provide this capability.) Delaying decisions about program implementation until run time is known as late binding; we will discuss it at greater length in Section 3.1.
相比之下,编译通常会带来更好的性能。通常,编译时做出的决定是运行时不需要做出的决定。例如,如果编译器可以保证变量x始终位于位置49378,它就可以生成机器语言指令,以便在源程序引用x时访问此位置。相比之下,解释器可能需要在每次访问x时在表中查找它,以找到它的位置。由于程序(的最终版本)仅编译一次,但通常执行多次,因此节省的成本可能非常可观,特别是如果解释器在循环的每次迭代中都做不必要的工作。
Compilation, by contrast, generally leads to better performance. In general, a decision made at compile time is a decision that does not need to be made at run time. For example, if the compiler can guarantee that variable x will always lie at location 49378, it can generate machine language instructions that access this location whenever the source program refers to x. By contrast, an interpreter may need to look x up in a table every time it is accessed, in order to find its location. Since the (final version of a) program is compiled only once, but generally executed many times, the savings can be substantial, particularly if the interpreter is doing unnecessary work in every iteration of a loop.
在实践中,我们看到了广泛的实现策略:
In practice one sees a broad spectrum of implementation strategies:
在 Basic 的一些早期实现中,手册实际上建议从程序中删除注释以提高其性能。这些实现是纯粹的解释器;它们每次执行程序的给定部分时都会重新读取(然后忽略)注释。它们没有初始翻译器。
In some very early implementations of Basic, the manual actually suggested removing comments from a program in order to improve its performance. These implementations were pure interpreters; they would re-read (and then ignore) the comments every time they executed a given part of the program. They had no initial translator.
有时,人们会听到 C++ 编译器被称为预处理器,大概是因为它生成的高级输出会被编译。我认为这是对术语的误用:编译器试图“理解”其源代码;而预处理器则不会。预处理器根据简单的模式匹配执行转换,并且可能会生成输出,这些输出在后续转换阶段运行时会生成错误消息。
Occasionally one would hear the C++ compiler referred to as a preprocessor, presumably because it generated high-level output that was in turn compiled. I consider this a misuse of the term: compilers attempt to “understand” their source; preprocessors do not. Preprocessors perform transformations based on simple pattern matching, and may well produce output that will generate error messages when run through a subsequent stage of translation.
正如这些例子中所表明的,编译器不一定会将高级编程语言翻译成机器语言。事实上,有些编译器接受的输入我们可能根本不会立即认为是程序。例如,像 T E X 这样的文本格式化程序会将高级文档描述编译成激光打印机或照排机的命令。(许多激光打印机本身包含预安装的 Postscript 页面描述语言解释器。)数据库系统的查询语言处理器会将 SQL 等语言翻译成对文件的原始操作。甚至还有编译器将逻辑级电路规范翻译成计算机芯片的照相掩模版。虽然本书的重点是命令式编程语言,但“编译”一词适用于当我们在充分分析输入含义的情况下从一种非平凡语言自动翻译成另一种语言时。
As some of these examples make clear, a compiler does not necessarily translate from a high-level programming language into machine language. Some compilers, in fact, accept inputs that we might not immediately think of as programs at all. Text formatters like TEX, for example, compile high-level document descriptions into commands for a laser printer or phototypesetter. (Many laser printers themselves contain pre-installed interpreters for the Postscript page-description language.) Query language processors for database systems translate languages like SQL into primitive operations on files. There are even compilers that translate logic-level circuit specifications into photographic masks for computer chips. Though the focus in this book is on imperative programming languages, the term “compilation” applies whenever we translate automatically from one nontrivial language to another, with full analysis of the meaning of the input.
编译器和解释器并不是孤立存在的。程序员可以通过许多其他工具来协助他们的工作。前面提到了汇编器、调试器、预处理器和链接器。每个程序员都熟悉编辑器。它们可能配备了交叉引用功能,使程序员能够在给定对象使用点的情况下找到定义该对象的点。漂亮的打印机有助于强制执行格式约定。样式检查器强制执行的句法或语义约定可能比编译器强制执行的约定更严格(参见探索 1.14)。配置管理工具有助于跟踪大型软件系统中单独编译的模块(的许多版本)之间的依赖关系。阅读工具不仅适用于文本,也适用于可能以二进制形式存储的中间语言。分析器和其他性能分析工具通常与调试器协同工作,以帮助识别程序中消耗大量计算时间的部分。
Compilers and interpreters do not exist in isolation. Programmers are assisted in their work by a host of other tools. Assemblers, debuggers, preprocessors, and linkers were mentioned earlier. Editors are familiar to every programmer. They may be augmented with cross-referencing facilities that allow the programmer to find the point at which an object is defined, given a point at which it is used. Pretty printers help enforce formatting conventions. Style checkers enforce syntactic or semantic conventions that may be tighter than those enforced by the compiler (see Exploration 1.14). Configuration management tools help keep track of dependences among the (many versions of) separately compiled modules in a large software system. Perusal tools exist not only for text but also for intermediate languages that may be stored in binary. Profilers and other performance analysis tools often work in conjunction with debuggers to help identify the pieces of a program that consume the bulk of its computation time.
在较旧的编程环境中,工具可能会根据用户的明确请求单独执行。例如,如果正在运行的程序因“总线错误”(无效地址)消息而异常终止,则用户可以选择调用调试器来检查操作系统转储的“核心”文件。然后,他或她可能会尝试通过设置断点、启用跟踪等来识别程序错误,并在调试器的控制下再次运行该程序。一旦发现错误,用户将调用编辑器进行适当的更改。然后,他或她将重新编译修改后的程序,可能借助配置管理器。
In older programming environments, tools may be executed individually, at the explicit request of the user. If a running program terminates abnormally with a “bus error” (invalid address) message, for example, the user may choose to invoke a debugger to examine the “core” file dumped by the operating system. He or she may then attempt to identify the program bug by setting breakpoints, enabling tracing and so on, and running the program again under the control of the debugger. Once the bug is found, the user will invoke the editor to make an appropriate change. He or she will then recompile the modified program, possibly with the help of a configuration manager.
现代环境提供了更多集成工具。当集成开发环境 (IDE) 中出现无效地址错误时,用户的屏幕上可能会出现一个新窗口,其中突出显示发生错误的源代码行。然后可以在此窗口中设置断点和跟踪,而无需明确调用调试器。无需明确调用编辑器即可对源代码进行更改。如果用户在进行更改后要求重新运行程序,则可以构建新版本,而无需明确调用编译器或配置管理器。
Modern environments provide more integrated tools. When an invalid address error occurs in an integrated development environment (IDE), a new window is likely to appear on the user's screen, with the line of source code at which the error occurred highlighted. Breakpoints and tracing can then be set in this window without explicitly invoking a debugger. Changes to the source can be made without explicitly invoking an editor. If the user asks to rerun the program after making changes, a new version may be built without explicitly invoking the compiler or configuration manager.
IDE 的编辑器可能包含语言语法知识,为所有标准控制结构提供模板,并在输入时检查语法。在内部,IDE 可能不仅维护程序的源代码和目标代码,还维护部分编译的内部表示。编辑源代码时,内部表示将自动更新 - 通常是增量更新(无需重新解析源代码的大部分内容)。在某些情况下,对程序的结构更改可能首先在内部表示中实现,然后自动反映在源代码中。
The editor for an IDE may incorporate knowledge of language syntax, providing templates for all the standard control structures, and checking syntax as it is typed in. Internally, the IDE is likely to maintain not only a program's source and object code, but also a partially compiled internal representation. When the source is edited, the internal representation will be updated automatically—often incrementally (without reparsing large portions of the source). In some cases, structural changes to the program may be implemented first in the internal representation, and then automatically reflected in the source.
IDE 是 Smalltalk 的基础——几乎不可能将该语言与其图形环境分开——自 20 世纪 80 年代以来,它一直被常规用于 Common Lisp。随着图形界面的普及,集成环境已在很大程度上取代了许多语言和系统的命令行工具。流行的开源 IDE 包括 Eclipse 和 NetBeans。商业系统包括 Microsoft 的 Visual Studio 环境和 Apple 的 XCode 环境。集成的大部分外观也可以在复杂的编辑器(如emacs )中实现。
IDEs are fundamental to Smalltalk—it is nearly impossible to separate the language from its graphical environment—and have been routinely used for Common Lisp since the 1980s. With the ubiquity of graphical interfaces, integrated environments have largely displaced command-line tools for many languages and systems. Popular open-source IDEs include Eclipse and NetBeans. Commercial systems include the Visual Studio environment from Microsoft and the XCode environment from Apple. Much of the appearance of integration can also be achieved within sophisticated editors such as emacs.
编译器是研究最深入的计算机程序之一。我们将在本书的其余部分,尤其是第 2 章、第 4 章、第 15 章和第17章中反复讨论它们。本节的其余部分提供了介绍性概述。
Compilers are among the most well-studied computer programs. We will consider them repeatedly throughout the rest of the book, and in chapters 2, 4, 15, and 17 in particular. The remainder of this section provides an introductory overview.
有时我们会听到有人将编译描述为一系列过程。过程是一个阶段或一组阶段,它们相对于编译的其余部分是序列化的:它在前面的阶段完成后才开始,并在任何后续阶段开始之前完成。如果需要,可以将过程编写为单独的程序,从文件读取其输入并将其输出写入文件。编译器通常分为几个过程,以便前端可以由多种机器(目标语言)的编译器共享,并且后端可以由多种源语言的编译器共享。在某些实现中,前端和后端可以由负责独立于语言和机器的代码改进的“中间端”分隔开。先前由于 20 世纪 80 年代中后期内存容量的急剧增加,编译器有时也被分为几个过程来尽量减少内存使用:每完成一个过程,下一个过程就可以重用其代码空间。
One will sometimes hear compilation described as a series of passes. A pass is a phase or set of phases that is serialized with respect to the rest of compilation: it does not start until previous phases have completed, and it finishes before any subsequent phases start. If desired, a pass may be written as a separate program, reading its input from a file and writing its output to a file. Compilers are commonly divided into passes so that the front end may be shared by compilers for more than one machine (target language), and so that the back end may be shared by compilers for more than one source language. In some implementations the front end and the back end may be separated by a “middle end” that is responsible for language- and machine-independent code improvement. Prior to the dramatic increases in memory sizes of the mid to late 1980s, compilers were also sometimes divided into passes to minimize memory usage: as each pass completed, the next could reuse its code space.
扫描又称为词法分析。扫描器的主要目的是通过减少输入的大小(字符比标记多得多)和删除空格等无关字符来简化解析器的任务。扫描器通常还会删除注释并用行号和列号标记标记,以便在后续阶段更容易生成良好的诊断。可以设计一个解析器以字符而不是标记作为输入 - 省去扫描器 - 但结果会很笨拙且缓慢。
Scanning is also known as lexical analysis. The principal purpose of the scanner is to simplify the task of the parser, by reducing the size of the input (there are many more characters than tokens) and by removing extraneous characters like white space. The scanner also typically removes comments and tags tokens with line and column numbers, to make it easier to generate good diagnostics in later phases. One could design a parser to take characters instead of tokens as input— dispensing with the scanner—but the result would be awkward and slow.
在扫描和解析过程中,编译器或解释器会检查程序的所有标记是否格式正确,以及标记序列是否符合上下文无关语法定义的语法。任何格式错误的标记(例如,C 语言中的123abc或$@foo )都会导致扫描器生成错误消息。任何语法无效的标记序列(例如,C 语言中的A = XYZ )都会导致解析器生成错误消息。
In the process of scanning and parsing, the compiler or interpreter checks to see that all of the program's tokens are well formed, and that the sequence of tokens conforms to the syntax defined by the context-free grammar. Any malformed tokens (e.g., 123abc or $@foo in C) should cause the scanner to produce an error message. Any syntactically invalid token sequence (e.g., A = X Y Z in C) should lead to an error message from the parser.
语义分析是发现程序中的含义。除其他功能外,语义分析器还可以识别同一标识符的多次出现是否意味着指向同一程序实体,并确保使用一致。在大多数语言中,它还会跟踪标识符和表达式的类型,以验证使用是否一致,并指导编译器后端的代码生成。
Semantic analysis is the discovery of meaning in a program. Among other things, the semantic analyzer recognizes when multiple occurrences of the same identifier are meant to refer to the same program entity, and ensures that the uses are consistent. In most languages it also tracks the types of both identifiers and expressions, both to verify consistent usage and to guide the generation of code in the back end of a compiler.
为了协助其工作,语义分析器通常会构建和维护一个符号表数据结构,该结构将每个标识符映射到已知的有关该标识符的信息。除其他事项外,此信息包括标识符的类型、内部结构(如果有)和范围(该标识符有效的程序部分)。
To assist in its work, the semantic analyzer typically builds and maintains a symbol table data structure that maps each identifier to the information known about it. Among other things, this information includes the identifier's type, internal structure (if any), and scope (the portion of the program in which it is valid).
使用符号表,语义分析器可以执行上下文无关语法和解析树的层次结构无法捕获的大量规则。例如,在 C 语言中,它会检查以确保
Using the symbol table, the semantic analyzer enforces a large variety of rules that are not captured by the hierarchical structure of the context-free grammar and the parse tree. In C, for example, it checks to make sure that
■ Every identifier is declared before it is used.
■ 未在不适当的上下文中使用任何标识符(将整数作为子例程调用、将字符串添加到整数、引用错误结构类型的字段等)。
■ No identifier is used in an inappropriate context (calling an integer as a subroutine, adding a string to an integer, referencing a field of the wrong type of struct, etc.).
■ 子例程调用提供了正确数量和类型的参数。
■ Subroutine calls provide the correct number and types of arguments.
■ Switch语句臂上的标签是不同的常量。
■ Labels on the arms of a switch statement are distinct constants.
■任何具有 非void返回类型的函数都会明确返回一个值。
■ Any function with a non-void return type returns a value explicitly.
在许多前端中,语义分析器的工作采用语义动作例程的形式,当解析器意识到已经到达语法规则中的特定点时,它会调用这些例程。
In many front ends, the work of the semantic analyzer takes the form of semantic action routines, invoked by the parser when it realizes that it has reached a particular point within a grammar rule.
当然,并非所有语义规则都可以在编译时(或在解释器的前端)进行检查。那些可以检查的规则称为语言的静态语义。那些必须在运行时(或在解释器的后期阶段)检查的规则称为语言的动态语义。C 语言几乎没有动态检查(其设计者选择了性能而不是安全性)。其他语言在运行时强制执行的规则示例包括:
Of course, not all semantic rules can be checked at compile time (or in the front end of an interpreter). Those that can are referred to as the static semantics of the language. Those that must be checked at run time (or in the later phases of an interpreter) are referred to as the dynamic semantics of the language. C has very little in the way of dynamic checks (its designers opted for performance over safety). Examples of rules that other languages enforce at run time include:
■ 除非变量已被赋值,否则它们永远不会在表达式中使用。10
■ Variables are never used in an expression unless they have been given a value.10
■ 除非指针指向一个有效对象,否则指针永远不会被取消引用。
■ Pointers are never dereferenced unless they refer to a valid object.
■ 数组下标表达式位于数组的边界内。
■ Array subscript expressions lie within the bounds of the array.
■ 算术运算不会溢出。
■ Arithmetic operations do not overflow.
当无法静态地执行规则时,编译器通常会生成代码以在运行时执行适当的检查,如果其中一项检查失败,则中止程序或生成异常。(异常将在9.4 节中讨论。)不幸的是,某些规则可能过于昂贵或无法执行,并且语言实现可能根本无法检查它们。在 Ada 中,违反此类规则的程序被称为错误的;在 C 中,其行为被称为未定义的。
When it cannot enforce rules statically, a compiler will often produce code to perform appropriate checks at run time, aborting the program or generating an exception if one of the checks then fails. (Exceptions will be discussed in Section 9.4.) Some rules, unfortunately, may be unacceptably expensive or impossible to enforce, and the language implementation may simply fail to check them. In Ada, a program that breaks such a rule is said to be erroneous; in C its behavior is said to be undefined.
在许多编译器中,带注释的语法树构成了从前端传递到后端的中间形式。在其他编译器中,语义分析以遍历树(通常是单次遍历)结束,从而生成其他中间形式。一种常见的中间形式由一个控制流图组成,其节点类似于简单理想化机器的汇编语言片段。我们将在第 15 章进一步考虑这个选项,其中我们的 GCD 程序的控制流图如图15.3所示。在一组相关的编译器中,几种语言的前端和几台机器的后端将共享一个通用的中间形式。
In many compilers, the annotated syntax tree constitutes the intermediate form that is passed from the front end to the back end. In other compilers, semantic analysis ends with a traversal of the tree (typically single pass) that generates some other intermediate form. One common such form consists of a control flow graph whose nodes resemble fragments of assembly language for a simple idealized machine. We will consider this option further in Chapter 15, where a control flow graph for our GCD program appears in Figure 15.3. In a suite of related compilers, the front ends for several languages and the back ends for several machines would share a common intermediate form.
代码生成器通常会将符号表包含在目标代码的不可执行部分中,以供符号调试器稍后使用。
Often a code generator will save the symbol table for later use by a symbolic debugger, by including it in a nonexecutable part of the target code.
代码改进通常被称为优化,尽管它很少使任何事情在绝对意义上达到最优。它是编译的一个可选阶段,其目标是将程序转换为一个新版本,以更高效地计算相同的结果——更快或使用更少的内存,或两者兼而有之。
Code improvement is often referred to as optimization, though it seldom makes anything optimal in any absolute sense. It is an optional phase of compilation whose goal is to transform a program into a new version that computes the same result more efficiently—more quickly or using less memory, or both.
一些改进与机器无关。这些改进可以作为中间形式的转换来执行。其他改进需要了解目标机器(或任何将以目标语言执行程序的东西)。这些必须作为目标程序的转换来执行。因此,代码改进通常会在编译器阶段列表中出现两次:一次是在语义分析和中间代码生成之后,另一次是在目标代码生成之后。
Some improvements are machine independent. These can be performed as transformations on the intermediate form. Other improvements require an understanding of the target machine (or of whatever will execute the program in the target language). These must be performed as transformations on the target program. Thus code improvement often appears twice in the list of compiler phases: once immediately after semantic analysis and intermediate code generation, and again immediately after target code generation.
在本章中,我们介绍了编程语言设计和实现的研究。我们考虑了为什么有这么多语言,是什么让它们成功或失败,如何对它们进行分类以供研究,以及读者可能从这项研究中获得什么好处。我们注意到语言设计和语言实现是紧密相连的。显然,实现必须符合语言的规则。同时,语言设计者必须考虑实现各种功能的难易程度,以及可能产生什么样的性能。
In this chapter we introduced the study of programming language design and implementation. We considered why there are so many languages, what makes them successful or unsuccessful, how they may be categorized for study, and what benefits the reader is likely to gain from that study. We noted that language design and language implementation are intimately tied to one another. Obviously an implementation must conform to the rules of the language. At the same time, a language designer must consider how easy or difficult it will be to implement various features, and what sort of performance is likely to result.
语言实现通常分为基于解释的实现和基于编译的实现。然而,我们注意到,这两种方法之间的区别并不明显,大多数实现都包含这两种方法的一点。一般来说,如果执行之前有一个翻译步骤,该步骤 (1) 全面分析程序的结构(语法)和含义(语义),并且 (2) 生成一个形式截然不同的等效程序,那么我们就说该语言是编译的。本书中的大部分实现材料都与编译有关。
Language implementations are commonly differentiated into those based on interpretation and those based on compilation. We noted, however, that the difference between these approaches is fuzzy, and that most implementations include a bit of each. As a general rule, we say that a language is compiled if execution is preceded by a translation step that (1) fully analyzes both the structure (syntax) and meaning (semantics) of the program, and (2) produces an equivalent program in a significantly different form. The bulk of the implementation material in this book pertains to compilation.
编译器通常由一系列阶段组成。前几个阶段(扫描、解析和语义分析)用于分析源程序。这些阶段统称为编译器的前端。最后几个阶段(目标代码生成和针对机器的代码改进)称为后端。它们用于构建目标程序(最好是快速程序),其语义与源代码相匹配。在前端和后端之间,优秀的编译器会执行大量与机器无关的代码改进;这个“中端”的阶段通常构成编译器的大部分代码,并占其执行时间的大部分。
Compilers are generally structured as a series of phases. The first few phases— scanning, parsing, and semantic analysis—serve to analyze the source program. Collectively these phases are known as the compiler's front end. The final few phases—target code generation and machine-specific code improvement—are known as the back end. They serve to build a target program—preferably a fast one—whose semantics match those of the source. Between the front end and the back end, a good compiler performs extensive machine-independent code improvement; the phases of this “middle end” typically comprise the bulk of the code of the compiler, and account for most of its execution time.
第 3、6、7、8、9 和 10 章是本书其余部分的核心。这些章从程序员和语言实现者的角度介绍了语言设计的基本问题。为了支持实现的讨论,第2章和第4章比本介绍中更详细地描述了编译器前端。第 5 章概述了汇编级架构。第 15至17章讨论了编译器后端,包括汇编器和链接器、运行时系统和代码改进技术。第11至14章介绍了其他语言范式。附录 A列出了文中提到的主要编程语言,以及家谱和参考书目。附录 B包含“设计和实现”边栏列表;附录 C包含编号示例列表。
Chapters 3, 6, 7, 8, 9, and 10 form the core of the rest of this book. They cover fundamental issues of language design, both from the point of view of the programmer and from the point of view of the language implementor. To support the discussion of implementations, Chapters 2 and 4 describe compiler front ends in more detail than has been possible in this introduction. Chapter 5 provides an overview of assembly-level architecture. Chapters 15 through 17 discuss compiler back ends, including assemblers and linkers, run-time systems, and code improvement techniques. Additional language paradigms are covered in Chapters 11 through 14. Appendix A lists the principal programming languages mentioned in the text, together with a genealogical chart and bibliographic references. Appendix B contains a list of “Design & Implementation” sidebars; Appendix C contains a list of numbered examples.
1.1 计算机程序中的错误可以根据检测到的时间进行分类,如果在编译时检测到错误,则根据编译器的哪个部分检测到错误进行分类。使用您最喜欢的命令式语言,给出以下每个示例。
1.1 Errors in a computer program can be classified according to when they are detected and, if they are detected at compile time, what part of the compiler detects them. Using your favorite imperative language, give an example of each of the following.
(a) A lexical error, detected by the scanner
(b) 解析器检测到语法错误
(b) A syntax error, detected by the parser
(c) 通过语义分析检测到的静态语义错误
(c) A static semantic error, detected by semantic analysis
(d) 编译器生成的代码检测到的动态语义错误
(d) A dynamic semantic error, detected by code generated by the compiler
(e) 编译器无法捕获的错误,也无法轻易生成代码来捕获的错误(这应该是违反语言定义的,而不仅仅是程序错误)
(e) An error that the compiler can neither catch nor easily generate code to catch (this should be a violation of the language definition, not just a program bug)
1.2 再次考虑 Niklaus Wirth 分发的 Pascal 工具集(示例 1.15)。成功构建机器语言版本的 Pascal 编译器后,原则上可以丢弃 P 代码解释器和 P 代码版本的编译器。为什么人们会选择不这样做呢?
1.2 Consider again the Pascal tool set distributed by Niklaus Wirth (Example 1.15). After successfully building a machine language version of the Pascal compiler, one could in principle discard the P-code interpreter and the P-code version of the compiler. Why might one choose not to do so?
1.3 Fortran 和 C 等命令式语言通常是编译型的,而脚本语言通常是解释型的,因为脚本语言中的许多问题直到运行时才能解决。解释仅仅是编译不可行时“不得不做的事情”吗?还是即使有编译器可用,解释语言实际上也有一些优势?
1.3 Imperative languages like Fortran and C are typically compiled, while scripting languages, in which many issues cannot be settled until run time, are typically interpreted. Is interpretation simply what one “has to do” when compilation is infeasible, or are there actually some advantages to interpreting a language, even when a compiler is available?
1.4 示例 1.20中的gcd程序也可以这样写:int main() { int i = getint(), j = getint(); while (i != j) { if (i > j) i = i % j; else j = j % i; } putint(i); }这个程序计算的结果是否相同?如果不是,你能修复它吗?在什么情况下你认为其中一个会更快?
1.4 The gcd program of Example 1.20 might also be written
int main() {
int i = getint(), j = getint();
while (i != j) {
if (i > j) i = i % j;
else j = j % i;
}
putint(i);
}
Does this program compute the same result? If not, can you fix it? Under what circumstances would you expect one or the other to be faster?
1.5 扩展示例 1.25 ,跟踪对输入 12 和 8 的gcd程序的解释。哪些语法树节点被访问,按什么顺序访问?
1.5 Expanding on Example 1.25, trace an interpretation of the gcd program on the inputs 12 and 8. Which syntax tree nodes are visited, in which order?
1.6 解释和代码生成都可以通过遍历语法树来完成。比较这两种遍历。它们在哪些方面相似/不同?
1.6 Both interpretation and code generation can be performed by traversal of a syntax tree. Compare these two kinds of traversals. In what ways are they similar/different?
1.7 在你的 C 语言本地实现中,整数的大小限制是多少?如果发生算术溢出,会发生什么?大小限制对程序从一台机器/编译器到另一台机器/编译器的可移植性有何影响?对于 Java、Ada、Pascal 和 Scheme,这些问题的答案有何不同?(您可能需要查找手册。)
1.7 In your local implementation of C, what is the limit on the size of integers? What happens in the event of arithmetic overflow? What are the implications of size limits on the portability of programs from one machine/compiler to another? How do the answers to these questions differ for Java? For Ada? For Pascal? For Scheme? (You may need to find a manual.)
1.8 Unix make实用程序允许程序员指定程序中单独编译的部分之间的依赖关系。如果文件A依赖于文件B,并且文件B被修改,则make推断必须重新编译A ,以防对B的任何更改会影响为A生成的代码。这种依赖管理有多准确?在什么情况下会导致不必要的工作?在什么情况下,它会无法重新编译需要重新编译的内容?
1.8 The Unix make utility allows the programmer to specify dependences among the separately compiled pieces of a program. If file A depends on file B and file B is modified, make deduces that A must be recompiled, in case any of the changes to B would affect the code produced for A. How accurate is this sort of dependence management? Under what circumstances will it lead to unnecessary work? Under what circumstances will it fail to recompile something that needs to be recompiled?
1.9 为什么很难判断程序是否正确?如何查找代码中的错误?测试可以发现哪些错误?哪些错误不能?(有关程序正确性的更正式概念,请参阅第 4 章末尾的参考书目注释。)
1.9 Why is it difficult to tell whether a program is correct? How do you go about finding bugs in your code? What kinds of bugs are revealed by testing? What kinds of bugs are not? (For more formal notions of program correctness, see the bibliographic notes at the end of Chapter 4.)
(a) 您学习的第一门编程语言是什么?如果您选择了它,为什么会选择它?如果是别人为您选择的,您认为他们为什么会选择它?您觉得该语言的哪些部分最难学?
(a) What was the first programming language you learned? If you chose it, why did you do so? If it was chosen for you by others, why do you think they chose it? What parts of the language did you find the most difficult to learn?
(b) 对于你最熟悉的语言(这可能是也可能不是你学到的第一门语言),列出三件你希望设计不同的事物。你认为它们为什么会这样设计?如果有机会重来,你会如何修复它们?会有什么负面影响吗,例如在编译器复杂性或程序执行速度方面?
(b) For the language with which you are most familiar (this may or may not be the first one you learned), list three things you wish had been differently designed. Why do you think they were designed the way they were? How would you fix them if you had the chance to do it over? Would there be any negative consequences, for example in terms of compiler complexity or program execution speed?
1.11 找一个主要使用图 1.1中不同类别语言的同学。(例如,如果你主要使用 C 语言,你可以找一个有 Lisp 经验的人。)比较一下笔记。在你们各自的经历中,编程最简单和最困难的方面是什么?选择一个简单的问题(例如,排序或识别图中的连通分量),并使用你最喜欢的每种语言来解决它。哪种解决方案更优雅(你们两个同意吗)?哪个更快?为什么?
1.11 Get together with a classmate whose principal programming experience is with a language in a different category of Figure 1.1. (If your experience is mostly in C, for example, you might search out someone with experience in Lisp.) Compare notes. What are the easiest and most difficult aspects of programming, in each of your experiences? Pick a simple problem (e.g., sorting, or identification of connected components in a graph) and solve it using each of your favorite languages. Which solution is more elegant (do the two of you agree)? Which is faster? Why?
1.12
1.12
(a) 如果您可以访问 Unix 系统,请使用−S命令行标志编译一个简单的程序。向生成的汇编语言文件添加注释以解释每条指令的用途。
(a) If you have access to a Unix system, compile a simple program with the −S command-line flag. Add comments to the resulting assembly language file to explain the purpose of each instruction.
(b) 现在使用-o命令行标志生成可重定位目标文件。使用适当的本地工具(特别是查找nm、objdump或使用符号调试器(例如gdb或dbx)来识别每行汇编程序对应的机器语言。
(b) Now use the −o command-line flag to generate a relocatable object file. Using appropriate local tools (look in particular for nm, objdump, or a symbolic debugger like gdb or dbx), identify the machine language corresponding to each line of assembler.
(c) 使用nm、objdump或类似工具,识别目标文件中未定义的外部符号。现在运行编译器直至完成,以生成可执行文件。最后,再次运行nm或objdump以查看部分 (b) 中的符号发生了什么。它们来自哪里 — 链接器如何解析它们?
(c) Using nm, objdump, or a similar tool, identify the undefined external symbols in your object file. Now run the compiler to completion, to produce an executable file. Finally, run nm or objdump again to see what has happened to the symbols in part (b). Where did they come from—how did the linker resolve them?
(d)使用 -v命令行标志再次运行编译器直至完成。您应该看到描述编译过程中调用的各种子程序的消息(某些编译器对此选项使用不同的字母;请查看手册页)。子程序可能包括预处理器、编译器本身的单独传递(通常为两次)、可能还有汇编器和链接器。如果可能,请自己单独运行这些子程序。它们中的哪一个生成了前面子问题中描述的文件?解释调用子程序的各种命令行标志的用途。
(d) Run the compiler to completion one more time, using the −v command-line flag. You should see messages describing the various subprograms invoked during the compilation process (some compilers use a different letter for this option; check the man page). The subprograms may include a preprocessor, separate passes of the compiler itself (often two), probably an assembler, and the linker. If possible, run these subprograms yourself, individually. Which of them produce the files described in the previous subquestions? Explain the purpose of the various command-line flags with which the subprograms were invoked.
1.13 编写一个程序,该程序会犯动态语义错误(例如,除以零、访问数组末尾以外的内容、取消引用空指针)。运行此程序时会发生什么?编译器是否为您提供了控制发生情况的选项?设计一个实验来评估运行时语义检查的成本。如果可能,请使用多种语言或编译器尝试此练习。
1.13 Write a program that commits a dynamic semantic error (e.g., division by zero, access off the end of an array, dereference of a null pointer). What happens when you run this program? Does the compiler give you options to control what happens? Devise an experiment to evaluate the cost of runtime semantic checks. If possible, try this exercise with more than one language or compiler.
1.14 C 语言被认为是一种相对“不安全”的高级语言。例如:它允许程序员以比其“更安全”的同类语言更多的方式混合不同大小和类型的操作数。Unix lint实用程序可用于搜索 C 程序中潜在的不安全构造。实际上,其他语言中编译器强制执行的许多规则在 C 语言中是可选的,并且由单独的程序强制执行(如果需要)。您如何看待这种方法?这是一个好主意吗?为什么或为什么不?
1.14 C has a reputation for being a relatively “unsafe” high-level language. For example: it allows the programmer to mix operands of different sizes and types in many more ways than its “safer” cousins. The Unix lint utility can be used to search for potentially unsafe constructs in C programs. In effect, many of the rules that are enforced by the compiler in other languages are optional in C, and are enforced (if desired) by a separate program. What do you think of this approach? Is it a good idea? Why or why not?
1.15 使用互联网搜索引擎或杂志索引服务,了解 Java 和 C# 的历史,包括 Sun 和 Microsoft 在 Java 标准化方面的冲突。有人声称 C# 至少在一定程度上是 Microsoft 试图破坏 Java 的传播。其他人则指出这两种语言在哲学和实践上的差异,并认为 C# 的优势不止于此。事后看来,您如何看待 Microsoft 寻求替代 Java 的决定?
1.15 Using an Internet search engine or magazine indexing service, read up on the history of Java and C#, including the conflict between Sun and Microsoft over Java standardization. Some have claimed that C# was, at least in part, an attempt by Microsoft to undermine the spread of Java. Others point to philosophical and practical differences between the languages, and argue that C# more than stands on its merits. In hindsight, how would you characterize Microsoft's decision to pursue an alternative to Java?
本书中面向编译器的章节试图传达编译器的功能,而不是解释如何构建编译器。其他文本中可以找到更详细的信息。主要选项包括 Aho 的作品等人 [ ALSU07 ]、Cooper 和 Torczon [ CT04 ] 以及 Fischer 等人 [ FCL10 ] 的著作。其他优秀但不太流行的著作包括 Appel [ App97 ] 和 Grune 等人 [ GBJ + 12 ] 的著作。关于编程语言设计的热门著作包括 Louden [ LL12 ]、Sebesta [ Seb15 ] 和 Sethi [ Set96 ]的著作。
The compiler-oriented chapters of this book attempt to convey a sense of what the compiler does, rather than explaining how to build one. A much greater level of detail can be found in other texts. Leading options include the work of Aho et al. [ALSU07], Cooper and Torczon [CT04], and Fischer et al. [FCL10]. Other excellent, though less current texts include those of Appel [App97] and Grune et al. [GBJ+12]. Popular texts on programming language design include those of Louden [LL12], Sebesta [Seb15], and Sethi [Set96].
关于编程语言历史的一些最好的信息可以在计算机协会在 1978、1993 和 2007 年主办的会议记录中找到 [ Wex78、Ass93、Ass07 ]。另一个很好的参考资料是 Horowitz 1987 年的文本 [ Hor87 ]。在季刊《 IEEE 计算史年鉴》中可以找到更广泛的历史资料。鉴于个人品味在编程语言设计中的重要性,一些语言比较不可避免地会带有措辞强烈的意见。早期的例子包括 Dijkstra [ Dij82 ]、Hoare [ Hoa81 ]、Kernighan [ Ker81 ] 和 Wirth [ Wir85a ]的著作。
Some of the best information on the history of programming languages can be found in the proceedings of conferences sponsored by the Association for Computing Machinery in 1978,1993, and 2007 [Wex78, Ass93, Ass07]. Another excellent reference is Horowitz's 1987 text [Hor87]. A broader range of historical material can be found in the quarterly IEEE Annals of the History of Computing. Given the importance of personal taste in programming language design, it is inevitable that some language comparisons should be marked by strongly worded opinions. Early examples include the writings of Dijkstra [Dij82], Hoare [Hoa81], Kernighan [Ker81], and Wirth [Wir85a].
许多现代软件开发都是在集成编程环境中进行的。这些环境的有影响力的前身包括 Symbolics Corp. 的 Genera Common Lisp 环境 [ WMWM87 ] 以及 Xerox Palo Alto 研究中心的 Smalltalk [ Gol84 ]、Interlisp [ TM81 ] 和 Cedar [ SZBH86 ] 环境。
Much modern software development takes place in integrated programming environments. Influential precursors to these environments include the Genera Common Lisp environment from Symbolics Corp. [WMWM87] and the Smalltalk [Gol84], Interlisp [TM81], and Cedar [SZBH86] environments at the Xerox Palo Alto Research Center.
与英语或中文等自然语言不同,计算机语言必须精确。其形式(语法)和含义(语义)都必须明确无误,这样程序员和计算机才能知道程序应该做什么。为了提供所需的精确度,语言设计者和实现者使用正式的语法和语义符号。为了便于在后面的章节中讨论语言特性,我们将首先介绍这种符号:本章介绍语法,第4 章介绍语义。
Unlike natural languages such as English or Chinese, computer languages must be precise. Both their form (syntax) and meaning (semantics) must be specified without ambiguity, so that both programmers and computers can tell what a program is supposed to do. To provide the needed degree of precision, language designers and implementors use formal syntactic and semantic notation. To facilitate the discussion of language features in later chapters, we will cover this notation first: syntax in the current chapter and semantics in Chapter 4.
当然,数字只是符号:纸上的墨迹或屏幕上的像素。它们本身没有任何意义。当我们说数字代表数学家定义的从零到九的自然数时,我们为数字添加了语义。或者,我们可以说它们代表颜色,或十进制日历中的星期几。这些将构成相同语法的替代语义。以类似的方式,我们通过将十进制、位值解释与每个自然数相关联来定义自然数的语义数字串。可以为有理数、(有限精度)实数、算术、赋值、控制流、声明以及实际上所有编程语言设计类似的语法规则和语义解释。
Of course, digits are only symbols: ink blobs on paper or pixels on a screen. They carry no meaning in and of themselves. We add semantics to digits when we say that they represent the natural numbers from zero to nine, as defined by mathematicians. Alternatively, we could say that they represent colors, or the days of the week in a decimal calendar. These would constitute alternative semantics for the same syntax. In a similar fashion, we define the semantics of natural numbers by associating a base-10, place-value interpretation with each string of digits. Similar syntax rules and semantic interpretations can be devised for rational numbers, (limited-precision) real numbers, arithmetic, assignments, control flow, declarations, and indeed all of programming languages.
区分语法和语义至少有两个好处。首先,不同的编程语言通常提供语义非常相似但语法非常不同的功能。如果能够识别不熟悉的语法下的常见(并且可能很熟悉)语义思想,学习一门新语言通常会容易得多。其次,编译器或解释器可以使用一些非常高效和优雅的算法来发现计算机程序的句法结构(但不是语义!),这些算法可用于驱动其余的编译或解释过程。
Distinguishing between syntax and semantics is useful for at least two reasons. First, different programming languages often provide features with very similar semantics but very different syntax. It is generally much easier to learn a new language if one is able to identify the common (and presumably familiar) semantic ideas beneath the unfamiliar syntax. Second, there are some very efficient and elegant algorithms that a compiler or interpreter can use to discover the syntactic structure (but not the semantics!) of a computer program, and these algorithms can be used to drive the rest of the compilation or interpretation process.
在本章中,我们将重点关注语法:如何指定编程语言的结构规则,以及编译器如何识别给定输入程序的结构。 这两个任务(指定语法规则和弄清给定程序是如何(以及是否)根据这些规则构建的)是不同的。 第一个任务主要与想要编写有效程序的程序员有关。 第二个任务主要与需要分析这些程序的编译器有关。 第一项任务依赖于正则表达式和上下文无关语法,它们指定如何生成有效程序。 第二项任务依赖于扫描器和解析器,它们识别程序结构。 我们在第 2.1 节中讨论第一个任务,在第 2.2和2.3节中讨论第二个任务。
In the current chapter we focus on syntax: how we specify the structural rules of a programming language, and how a compiler identifies the structure of a given input program. These two tasks—specifying syntax rules and figuring out how (and whether) a given program was built according to those rules—are distinct. The first is of interest mainly to programmers, who want to write valid programs. The second is of interest mainly to compilers, which need to analyze those programs. The first task relies on regular expressions and context-free grammars, which specify how to generate valid programs. The second task relies on scanners and parsers, which recognize program structure. We address the first of these tasks in Section 2.1, the second in Sections 2.2 and 2.3.
在第 2.4 节(主要在配套网站上)中,我们深入研究了扫描和解析的基本理论。从理论角度来说,扫描器是确定性有限自动机(DFA),可识别编程语言的标记。解析器是确定性下推自动机(PDA),可识别语言的上下文无关语法。事实证明,人们可以从正则表达式和上下文无关语法自动生成扫描器和解析器。此任务由 Unix 的lex和yacc 2等工具执行。在计算机科学中,可能没有其他地方能如此清晰、如此令人信服地将理论与实践联系起来。
In Section 2.4 (largely on the companion site) we take a deeper look at the formal theory underlying scanning and parsing. In theoretical parlance, a scanner is a deterministic finite automaton (DFA) that recognizes the tokens of a programming language. A parser is a deterministic push-down automaton (PDA) that recognizes the language's context-free syntax. It turns out that one can generate scanners and parsers automatically from regular expressions and context-free grammars. This task is performedbytools like Unix's lex and yacc,2 among others. Possibly nowhere else in computer science is the connection between theory and practice so clear and so compelling.
语法的正式规范需要一套规则。语法的复杂程度(表达能力)取决于我们可以使用哪种规则。事实证明,我们直观地认为的标记可以用三种正式规则从单个字符构造出来:连接、交替(在一组有限的备选方案中进行选择)和所谓的“克莱尼闭包”(重复任意次数)。指定我们直观认为的语法的其余大部分内容需要一种额外的规则:递归(从相同构造的更简单实例创建构造)。任何可以根据前三个规则定义的字符串集合称为正则集,有时也称为正则语言。正则集由正则表达式生成并被扫描器识别。任何可以通过添加递归来定义的字符串集合称为上下文无关语言(CFL)。上下文无关语言由上下文无关语法(CFG)生成并被解析器识别。(这里的术语可能令人困惑。“语言”一词的含义差异很大,取决于我们谈论的是“形式”语言 [例如,正则语言或上下文无关语言] 还是编程语言。形式语言只是一组字符串,没有伴随的语义。)
Formal specification of syntax requires a set of rules. How complicated (expressive) the syntax can be depends on the kinds of rules we are allowed to use. It turns out that what we intuitively think of as tokens can be constructed from individual characters using just three kinds of formal rules: concatenation, alternation (choice among a finite set of alternatives), and so-called “Kleene closure” (repetition an arbitrary number of times). Specifying most of the rest of what we intuitively think of as syntax requires one additional kind of rule: recursion (creation of a construct from simpler instances of the same construct). Any set of strings that can be defined in terms of the first three rules is called a regular set, or sometimes a regular language. Regular sets are generated by regular expressions and recognized by scanners. Any set of strings that can be defined if we add recursion is called a context-free language (CFL). Context-free languages are generated by context-free grammars (CFGs) and recognized by parsers. (Terminology can be confusing here. The meaning of the word “language” varies greatly, depending on whether we're talking about “formal” languages [e.g., regular or context-free], or programming languages. A formal language is just a set of strings, with no accompanying semantics.)
标记是程序的基本构造块——具有独立含义的最短字符串。标记有多种类型,包括关键字、标识符、符号和各种类型的常量。某些类型的标记(例如,增量运算符)仅对应一串字符。其他类型的标记(例如,标识符)对应于一组具有某种共同形式的字符串。(在大多数语言中,关键字是特殊的字符串,它们具有作为标识符的正确形式,但保留用于特殊用途。)我们将非正式地使用“标记”一词来指代一般类型(标识符、增量运算符)和特定字符串(foo, ++);它们之间的区别应该从上下文中很清楚。
Tokens are the basic building blocks of programs—the shortest strings of characters with individual meaning. Tokens come in many kinds, including keywords, identifiers, symbols, and constants of various types. Some kinds of token (e.g., the increment operator) correspond to only one string of characters. Others (e.g., identifier) correspond to a set of strings that share some common form. (In most languages, keywords are special strings of characters that have the right form to be identifiers, but are reserved for special purposes.) We will use the word “token” informally to refer to both the generic kind (an identifier, the increment operator) and the specific string (foo, ++); the distinction between these should be clear from context.
为了指定标记,我们使用正则表达式的符号。正则表达式是下列之一:
To specify tokens, we use the notation of regular expressions. A regular expression is one of the following:
2. 空串,记为ε
2. The empty string, denoted ε
3. 两个正则表达式相邻,即第一个正则表达式生成的任意字符串后跟(连接)第二个正则表达式生成的任意字符串
3. Two regular expressions next to each other, meaning any string generated by the first one followed by (concatenated with) any string generated by the second one
4. 两个正则表达式用竖线(|)分隔,表示第一个正则表达式生成的任意字符串,或者第二个正则表达式生成的任意字符串
4. Two regular expressions separated by a vertical bar (|), meaning any string generated by the first one or any string generated by the second one
5. 正则表达式后跟一个 Kleene 星号,表示由星号前面的表达式生成的零个或多个字符串的连接
5. A regular expression followed by a Kleene star, meaning the concatenation of zero or more strings generated by the expression in front of the star
括号用于避免各个子表达式的开始和结束位置产生歧义。3
Parentheses are used to avoid ambiguity about where the various subexpressions start and end.3
在某些语言(例如 Perl、Python 和 Ruby;C 及其后代)中,标识符和关键字中的大小写字母被视为不同的,而在其他语言(例如 Ada、Common Lisp 和 Fortran)中则视为相同。因此,foo、Foo和FOO在 Ada 中都表示相同的标识符,但在 C 中则表示不同的标识符。Modula-2 和 Modula-3 要求关键字和预定义(内置)标识符必须大写;C 及其后代要求它们必须小写。少数语言只允许标识符中使用字母和数字。大多数允许使用下划线。少数语言(尤其是 Lisp)允许使用各种其他字符。某些语言(例如 Java 和 C#)对名称中大小写字母的使用有标准(但可选)的约定。5
Upper- and lowercase letters in identifiers and keywords are considered distinct in some languages (e.g., Perl, Python, and Ruby; C and its descendants), and identical in others (e.g., Ada, Common Lisp, and Fortran). Thus foo, Foo, and FOO all represent the same identifier in Ada, but different identifiers in C. Modula-2 and Modula-3 require keywords and predefined (built-in) identifiers to be written in uppercase; C and its descendants require them to be written in lowercase. A few languages allow only letters and digits in identifiers. Most allow underscores. A few (notably Lisp) allow a variety of additional characters. Some languages (e.g., Java and C#) have standard (but optional) conventions on the use of upper- and lowercase letters in names.5
随着计算的全球化,非拉丁字符集变得越来越重要。许多现代语言(包括 C、C++、Ada 95、Java、C# 和 Fortran 2003)都引入了对多字节字符集的明确支持,这些字符集通常基于 Unicode 和 ISO/IEC 10646 国际标准。大多数现代编程语言允许非拉丁字符出现在注释和字符串中;越来越多的语言也允许它们出现在标识符中。跨字符集的可移植性和针对给定字符集的本地化的约定可能非常复杂,特别是当需要各种形式的向后兼容性时(C99 基本原理用整整五页来讨论这个主题 [ Int03a,第 19-23 页]);我们在这里主要忽略这些问题。
With the globalization of computing, non-Latin character sets have become increasingly important. Many modern languages, including C, C++, Ada 95, Java, C#, and Fortran 2003 have introduced explicit support for multibyte character sets, generally based on the Unicode and ISO/IEC 10646 international standards. Most modern programming languages allow non-Latin characters to appear within comments and character strings; an increasing number allow them in identifiers as well. Conventions for portability across character sets and for localization to a given character set can be surprisingly complex, particularly when various forms of backward compatibility are required (the C99 Rationale devotes five full pages to this subject [Int03a, pp. 19–23]); for the most part we ignore such issues here.
某些语言实现对标识符的最大长度施加了限制,但大多数语言都避免了这种不必要的限制。大多数现代语言也或多或少是自由格式的,这意味着程序只是一系列标记:重要的是它们相对于彼此的顺序,而不是它们在打印行或页面中的物理位置。标记之间的“空白”(空格、制表符、回车符以及换行符和分页符)通常会被忽略,除非需要将一个标记与下一个标记分开。
Some language implementations impose limits on the maximum length of identifiers, but most avoid such unnecessary restrictions. Most modern languages are also more or less free format, meaning that a program is simply a sequence of tokens: what matters is their order with respect to one another, not their physical position within a printed line or page. “White space” (blanks, tabs, carriage returns, and line and page feed characters) between tokens is usually ignored, except to the extent that it is needed to separate one token from the next.
这些规则有几个值得注意的例外。一些语言实现限制了一行的最大长度,以允许编译器将当前行存储在固定长度的缓冲区中。Fortran 90 之前的 Fortran 方言使用固定格式,每行 72 个字符(曾经存储程序的打孔卡的宽度),并且行内的不同列保留用于不同用途。在其他几种语言中,换行符用于分隔语句,包括 Go、Haskell、Python 和 Swift。Haskell 和 Python 也赋予缩进特殊的意义。例如,循环主体恰好由缩进比循环头更远的后续行组成。
There are a few noteworthy exceptions to these rules. Some language implementations limit the maximum length of a line, to allow the compiler to store the current line in a fixed-length buffer. Dialects of Fortran prior to Fortran 90 use a fixed format, with 72 characters per line (the width of a paper punch card, on which programs were once stored), and with different columns within the line reserved for different purposes. Line breaks serve to separate statements in several other languages, including Go, Haskell, Python, and Swift. Haskell and Python also give special significance to indentation. The body of a loop, for example, consists of precisely those subsequent lines that are indented farther than the header of the loop.
许多读者都熟悉Unix 中的grep系列工具、各种文本编辑器的搜索功能或 Perl、Python、Ruby、awk和sed等脚本语言和工具中的正则表达式。其中大多数都为正则表达式的表示法提供了丰富的扩展。某些扩展(例如“零次或一次出现”或“除空格以外的任何内容”的简写)不会改变表示法的功能。其他扩展(例如,要求在输入字符串的后面出现与表达式的前面部分匹配的相同字符序列的第二次出现)增强了表示法的功能,因此它不再局限于生成正则集。其他扩展的设计目的不是增加表示法的表现力,而是将其与其他语言功能联系起来。例如,在许多工具中,可以将正则表达式的各个部分括起来,这样当一个字符串与其匹配时,相应子字符串的内容就会被分配到命名的局部变量中。我们将在第 14.4.2 节的脚本语言背景下再次讨论这些问题。
Many readers will be familiar with regular expressions from the grep family of tools in Unix, the search facilities of various text editors, or such scripting languages and tools as Perl, Python, Ruby, awk, and sed. Most of these provide a rich set of extensions to the notation of regular expressions. Some extensions, such as shorthand for “zero or one occurrences” or “anything other than white space,” do not change the power of the notation. Others, such as the ability to require a second occurrence, later in the input string, of the same character sequence that matched an earlier part of the expression, increase the power of the notation, so that it is no longer restricted to generating regular sets. Still other extensions are designed not to increase the expressiveness of the notation but rather to tie it to other language facilities. In many tools, for example, one can bracket portions of a regular expression in such a way that when a string is matched against it the contents of the corresponding substrings are assigned into named local variables. We will return to these issues in Section 14.4.2, in the context of scripting languages.
上下文无关文法中的每个规则称为一个产生式。产生式左侧的符号称为变量或非终结符。可以有任意数量的产生式具有相同的左侧。组成从文法得出的字符串的符号称为终结符(此处以打字机字体显示)。它们不能出现在任何产生式的左侧。在编程语言中,上下文无关文法的终结符是该语言的标记。其中一个非终结符(通常是第一个产生式左侧的非终结符)称为起始符号。它命名由整个文法定义的构造。
Each of the rules in a context-free grammar is known as a production. The symbols on the left-hand sides of the productions are known as variables, or nonterminals. There may be any number of productions with the same left-hand side. Symbols that are to make up the strings derived from the grammar are known as terminals (shown here in typewriter font). They cannot appear on the left-hand side of any production. In a programming language, the terminals of the context-free grammar are the language's tokens. One of the nonterminals, usually the one on the left-hand side of the first production, is called the start symbol. It names the construct defined by the overall grammar.
许多标记(例如上面的id和number)有多种可能的拼写(即,可能由多种可能的字符串表示)。解析器对这些不知情;它不会区分一个标识符与另一个标识符。然而,语义分析器会区分它们;扫描器必须保存每个这样的“有趣”标记的拼写以供以后使用。
Many tokens, such as id and number above, have many possible spellings (i.e., may be represented by many possible strings of characters). The parser is oblivious to these; it does not distinguish one identifier from another. The semantic analyzer does distinguish them, however; the scanner must save the spelling of each such “interesting” token for later use.
上下文无关语法向我们展示了如何生成语法上有效的终结符字符串:从起始符号开始。选择一个起始符号在左侧的产生式;用该产生式的右侧替换起始符号。现在在结果字符串中选择一个非终结符A ,选择一个左侧有A的产生式P ,用P的右侧替换A。重复此过程,直到没有非终结符剩余。
A context-free grammar shows us how to generate a syntactically valid string of terminals: Begin with the start symbol. Choose a production with the start symbol on the left-hand side; replace the start symbol with the right-hand side of that production. Now choose a nonterminal A in the resulting string, choose a production P with A on its left-hand side, and replace A with the right-hand side of P. Repeat this process until no nonterminals remain.
一系列替换操作,展示如何从起始符号导出一串终结符,称为一次推导。沿途的每一串符号称为一个句型。最终的句型仅由终结符组成,称为推导的产物。我们有时会省略中间步骤,写为expr ⇒* slope * x + interval,其中元符号⇒*表示“在零次或多次替换后导出”。在这个特定的推导中,我们在每个步骤都选择用某个产生式的右边替换最右边的非终结符。这种替换策略导致最右边的推导。还有许多其他可能的推导,包括最左边的推导和介于两者之间的选项。
A series of replacement operations that shows how to derive a string of terminals from the start symbol is called a derivation. Each string of symbols along the way is called a sentential form. The final sentential form, consisting of only terminals, is called the yield of the derivation. We sometimes elide the intermediate steps and write expr ⇒* slope * x + intercept, where the metasymbol ⇒* means “derives after zero or more replacements.” In this particular derivation, we have chosen at each step to replace the right-most nonterminal with the right-hand side of some production. This replacement strategy leads to a right-most derivation. There are many other possible derivations, including left-most and options in-between.
我们在第 1 章中看到,我们可以用解析树的形式来表示推导。解析树的根是语法的起始符号。树的叶子是其收益。每个内部节点及其子节点都表示产生式的使用。
We saw in Chapter 1 that we can represent a derivation graphically as a parse tree. The root of the parse tree is the start symbol of the grammar. The leaves of the tree are its yield. Each internal node, together with its children, represents the use of a production.
稍加思考就会发现,对于任何给定的上下文无关语言,都有无数个上下文无关语法。9但是,有些语法比其他语法有用得多。在本文中,我们将避免使用歧义语法(尽管大多数解析器生成器都允许使用它们,方法是使用消歧规则)。我们还将避免使用所谓的无用符号:不能生成任何终结符串的非终结符,或不能出现在任何派生结果中的终结符。
A moment's reflection will reveal that there are infinitely many context-free grammars for any given context-free language.9 Some grammars, however, are much more useful than others. In this text we will avoid the use of ambiguous grammars (though most parser generators allow them, by means of disambiguating rules). We will also avoid the use of so-called useless symbols: nonterminals that cannot generate any string of terminals, or terminals that cannot appear in the yield of any derivation.
在为一种编程语言设计文法时,我们一般会尝试找到一种能够反映程序内部结构的文法,以便编译器的其余部分能够使用它。(我们将在2.3.2 节中看到,我们还会尝试找到一种能够高效解析的文法,这可能有点困难。)结构在算术表达式中尤为重要,我们可以使用产生式来捕获各种运算符的结合性和优先级。结合性告诉我们,大多数语言中的运算符都是从左到右分组的,因此10 − 4 − 3表示(10 − 4) − 3 ,而不是10 − (4 − 3)。优先级告诉我们,大多数语言中的乘法和除法比加法和减法分组更紧密,因此3 + 4 * 5表示3 + (4 * 5),而不是(3 + 4) * 5。 (这些规则并不通用;我们将在第 6.1.1 节中再次考虑它们。)
When designing the grammar for a programming language, we generally try to find one that reflects the internal structure of programs in a way that is useful to the rest of the compiler. (We shall see in Section 2.3.2 that we also try to find one that can be parsed efficiently, which can be a bit of a challenge.) One place in which structure is particularly important is in arithmetic expressions, where we can use productions to capture the associativity and precedence of the various operators. Associativity tells us that the operators in most languages group left to right, so that 10 − 4 − 3 means (10 − 4) − 3 rather than 10 − (4 − 3). Precedence tells us that multiplication and division in most languages group more tightly than addition and subtraction, so that 3 + 4 * 5 means 3 + (4 * 5) rather than (3 + 4) * 5. (These rules are not universal; we will consider them again in Section 6.1.1.)
编程语言的扫描器和解析器共同负责发现程序的语法结构。这个发现过程或语法分析是将程序翻译成目标语言的等效程序的必要的第一步。(这也是直接解释程序的第一步。一般来说,在本书的其余部分,我们将重点关注编译,而不是解释。我们将要讨论的大部分内容要么明显适用于解释,要么显然与解释无关。)
Together, the scanner and parser for a programming language are responsible for discovering the syntactic structure of a program. This process of discovery, or syntax analysis, is a necessary first step toward translating the program into an equivalent program in the target language. (It's also the first step toward interpreting the program directly. In general, we will focus on compilation, rather than interpretation, for the remainder of the book. Most of what we shall discuss either has an obvious application to interpretation, or is obviously irrelevant to it.)
通过将输入字符分组为标记,扫描器大大减少了计算量更大的解析器必须检查的单个项目的数量。此外,扫描器通常会删除注释(因此解析器不必担心它们出现在整个上下文无关语法中 - 参见练习 2.20);保存“有趣”标记的文本,如标识符、字符串和数字文字;并使用行号和列号标记标记,以便在后续阶段更轻松地生成高质量的错误消息。
By grouping input characters into tokens, the scanner dramatically reduces the number of individual items that must be inspected by the more computationally intensive parser. In addition, the scanner typically removes comments (so the parser doesn't have to worry about them appearing throughout the context-free grammar—see Exercise 2.20); saves the text of “interesting” tokens like identifiers, strings, and numeric literals; and tags tokens with line and column numbers, to make it easier to generate high-quality error messages in subsequent phases.
通常,我们在每次调用扫描器时都接受最长的标记。因此foobar始终是foobar,而永远不会是f或foo或 f oob。更重要的是,在 C 语言等语言中,3.14159是实数,而永远不会是3、 . 和14159。空格(空格、制表符、换行符、注释)通常会被忽略,除非它分隔标记(例如,foo bar与foobar不同)。
As a rule, we accept the longest possible token in each invocation of the scanner. Thus foobar is always foobar and never f or foo or foob. More to the point, in a language like C, 3.14159 is a real number and never 3, ., and 14159. White space (blanks, tabs, newlines, comments) is generally ignored, except to the extent that it separates tokens (e.g., foo bar is different from foobar).
图 2.5可以相当容易地扩展以概述某些较大编程语言的扫描器。然后可以手工充实结果以创建某些实现语言的代码。生产编译器通常使用这种临时扫描器;代码快速而紧凑。然而,在语言开发过程中,通常最好以更结构化的方式构建扫描器,作为有限自动机的显式表示。有限自动机可以从一组正则表达式自动生成,从而当标记定义发生变化时可以轻松地重新生成扫描器。
Figure 2.5 could be extended fairly easily to outline a scanner for some larger programming language. The result could then be fleshed out, by hand, to create code in some implementation language. Production compilers often use such ad hoc scanners; the code is fast and compact. During language development, however, it is usually preferable to build a scanner in a more structured way, as an explicit representation of a finite automaton. Finite automata can be generated automatically from a set of regular expressions, making it easy to regenerate a scanner when token definitions change.
虽然有限自动机原则上可以手写,但更常见的是使用扫描器生成器工具从一组正则表达式自动构建一个。对于我们的计算器语言,我们希望将示例 2.9中的正则表达式转换为图 2.6中的自动机。该自动机具有理想的属性,即其动作是确定性的:在任何给定状态下,对于给定的输入字符,永远不会有超过一个可能的输出转换(箭头)由该字符标记。然而事实证明,没有明显的一步算法可以将一组正则表达式转换为等效的确定性有限自动机 (DFA)。典型的扫描器生成器将转换作为一系列三个独立的步骤来实现。
While a finite automaton can in principle be written by hand, it is more common to build one automatically from a set of regular expressions, using a scanner generator tool. For our calculator language, we should like to covert the regular expressions of Example 2.9 into the automaton of Figure 2.6. That automaton has the desirable property that its actions are deterministic: in any given state with a given input character there is never more than one possible outgoing transition (arrow) labeled by that character. As it turns out, however, there is no obvious one-step algorithm to convert a set of regular expressions into an equivalent deterministic finite automaton (DFA). The typical scanner generator implements the conversion as a series of three separate steps.
第一步将正则表达式转换为非确定性有限自动机 (NFA)。NFA 与 DFA 类似,不同之处在于 (1) 给定字符标记的给定状态可能存在多个转换,以及 (2) 可能存在所谓的epsilon 转换:箭头用空字符串符号标记,例如。如果存在从起始状态到终止状态的路径,并且该路径的非 epsilon 转换按顺序用标记的字符标记,则称 NFA 接受输入字符串(标记)。
The first step converts the regular expressions into a nondeterministic finite automaton (NFA). An NFA is like a DFA except that (1) there maybe more than one transition out of a given state labeled by a given character, and (2) there may be so-called epsilon transitions: arrows labeled by the empty string symbol, e. The NFA is said to accept an input string (token) if there exists a path from the start state to a final state whose non-epsilon transitions are labeled, in order, by the characters of the token.
为了避免搜索所有可能的路径以找到“有效”的路径,扫描器生成器的第二步是将 NFA 转换为等效的 DFA:一个接受相同语言的自动机,但其中没有 epsilon 转换,并且没有带有多个由相同字符标记的传出转换的状态。第三步是空间优化,生成具有尽可能少的状态数的最终 DFA。
To avoid the need to search all possible paths for one that “works,” the second step of a scanner generator translates the NFA into an equivalent DFA: an automaton that accepts the same language, but in which there are no epsilon transitions, and no states with more than one outgoing transition labeled by the same character. The third step is a space optimization that generates a final DFA with the minimum possible number of states.
在我们的示例中,DFA 最终比 NFA 小,但这只是因为我们的常规语言非常简单。理论上,DFA 中的状态数可能是 NFA 中状态数的指数,但这种极端情况在实践中也不常见。对于编程语言扫描器,DFA 往往比 NFA 大,但并不离谱。我们将在 C-2.4.1 节中更详细地考虑空间复杂性。
In our example, the DFA ends up being smaller than the NFA, but this is only because our regular language is so simple. In theory, the number of states in the DFA may be exponential in the number of states in the NFA, but this extreme is also uncommon in practice. For a programming language scanner, the DFA tends to be larger than the NFA, but not outlandishly so. We consider space complexity in more detail in Section C-2.4.1.
我们可以通过两种主要方式中的任一种实现一个扫描器,以明确捕获 DFA 的“圆圈和箭头”结构。一种方式是使用goto或嵌套case (switch)语句将自动机嵌入到程序的控制流中;另一种方式(如下一小节所述)使用表和驱动程序。一般来说,手写自动机倾向于使用嵌套 case 语句,而大多数自动生成的自动机使用表。表很难手动创建,但比在程序内部创建代码更容易。同样,嵌套 case 语句比图 2.5中的临时方法更容易编写和调试,尽管效率不如图 2.5 那么高。Unix 的lex/flex工具生成包含表和自定义驱动程序的 C 语言输出。
We can implement a scanner that explicitly captures the “circles-and-arrows” structure of a DFA in either of two main ways. One embeds the automaton in the control flow of the program using gotos or nested case (switch) statements; the other, described in the following subsection, uses a table and a driver. As a general rule, handwritten automata tend to use nested case statements, while most automatically generated automata use tables. Tables are hard to create by hand, but easier than code to create from within a program. Likewise, nested case statements are easier to write and to debug than the ad hoc approach of Figure 2.5, if not quite as efficient. Unix's lex/flex tool produces C language output containing tables and a customized driver.
代码的两个方面通常与形式有限自动机的严格形式不同。一个是关键字的处理。另一个是需要提前预知何时标记可以有效地扩展两个或更多个附加字符,但不能只扩展一个。
Two aspects of the code typically deviate from the strict form of a formal finite automaton. One is the handling of keywords. The other is the need to peek ahead when a token can validly be extended by two or more additional characters, but not by only one.
如第 2.1.1 节开头所述,大多数语言中的关键字看起来就像标识符,但保留用于特殊用途(一些作者使用术语“保留字”而不是“关键字”)。可以编写一个区分关键字和标识符的有限自动机,但它需要很多状态(参见练习 2.3)。因此,大多数扫描器(无论是手写还是自动生成的)都将关键字视为标识符规则的“例外”。在返回之前解析器的标识符,扫描器会在哈希表或 trie(分支路径树)中查找它,以确保它不是真正的关键字。10
As noted at the beginning of Section 2.1.1, keywords in most languages look just like identifiers, but are reserved for a special purpose (some authors use the term reserved word instead of keyword). It is possible to write a finite automaton that distinguishes between keywords and identifiers, but it requires a lot of states (see Exercise 2.3). Most scanners, both handwritten and automatically generated, therefore treat keywords as “exceptions” to the rule for identifiers. Before returning an identifier to the parser, the scanner looks it up in a hash table or trie (a tree of branching paths) to make sure it isn't really a keyword.10
在 C 语言中,点字符问题可以很容易地作为特殊情况处理。在需要大量前瞻的语言中,扫描器可以采用更通用的方法。在任何歧义情况下,它都假设可以使用较长的标记,但会记住较短的标记可能在过去的某个时间点被识别。它还会缓冲较短标记末尾之后读取的所有字符。如果乐观的假设导致扫描器进入错误状态,它会“取消读取”缓冲的字符,以便稍后再次看到它们,并返回较短的标记。
In C, the dot character problem can easily be handled as a special case. In languages requiring larger amounts of look-ahead, the scanner can take a more general approach. In any case of ambiguity, it assumes that a longer token will be possible, but remembers that a shorter token could have been recognized at some point in the past. It also buffers all characters read beyond the end of the shorter token. If the optimistic assumption leads the scanner into an error state, it “unreads” the buffered characters so that they will be seen again later, and returns the shorter token.
图 2.11中的代码明确地认识到了词汇错误的可能性。在某些情况下,输入的下一个字符可能既不是当前标记的可接受延续,也不是另一个标记的开始。在这种情况下,扫描器必须打印一条错误消息并执行某种恢复,以便编译可以继续,即使只是为了查找其他错误。幸运的是,词汇错误相对罕见——大多数字符序列确实对应于标记序列——并且相对容易处理。最常见的方法是简单地 (1) 丢弃当前无效的标记;(2) 向前跳,直到找到可以合法开始新标记的字符;(3) 重新启动扫描算法;(4) 依靠解析器的错误恢复机制来应对任何结果标记序列在语法上无效的情况。当然,错误恢复的需求并不是表驱动扫描器所独有的;任何扫描器都必须应对错误。我们没有在图 2.5中展示代码,但在实践中它必须存在。
The code in Figure 2.11 explicitly recognizes the possibility of lexical errors. In some cases the next character of input maybe neither an acceptable continuation of the current token nor the start of another token. In such cases the scanner must print an error message and perform some sort of recovery so that compilation can continue, if only to look for additional errors. Fortunately, lexical errors are relatively rare—most character sequences do correspond to token sequences—and relatively easy to handle. The most common approach is simply to (1) throw away the current, invalid token; (2) skip forward until a character is found that can legitimately begin a new token; (3) restart the scanning algorithm; and (4) count on the error-recovery mechanism of the parser to cope with any cases in which the resulting sequence of tokens is not syntactically valid. Of course the need for error recovery is not unique to table-driven scanners; any scanner must cope with errors. We did not show the code in Figure 2.5, but it would have to be there in practice.
图 2.11中的代码还表明,扫描器必须返回找到的标记类型及其字符串映像(拼写);同样,此要求适用于所有类型的扫描器。对于某些标记,字符串映像是多余的:毕竟,所有分号看起来都一样,所有while关键字也是如此。但是,对于其他标记(例如,标识符、字符串和数字常量),映像是语义分析所必需的。它对于错误消息也很有用:“未声明的标识符”不如“ foo未声明”好。
The code in Figure 2.11 also shows that the scanner must return both the kind of token found and its character-string image (spelling); again this requirement applies to all types of scanners. For some tokens the character-string image is redundant: all semicolons look the same, after all, as do all while keywords. For other tokens, however (e.g., identifiers, character strings, and numeric constants), the image is needed for semantic analysis. It is also useful for error messages: “undeclared identifier” is not as nice as “foo has not been declared.”
某些语言和语言实现允许程序包含称为pragma 的构造,这些构造为编译器提供指令或提示。不会改变程序语义(仅改变编译过程)的 pragma 有时被称为重要注释。在某些语言中,这个名称也很合适,因为像注释一样,pragma 可以出现在源程序中的任何位置。在这种情况下,它们通常由扫描器处理:允许它们出现在语法中的任何位置会使解析器变得非常复杂。在大多数语言中,然而,指令只允许出现在语法中某些明确定义的位置。在这种情况下,它们最好由解析器或语义分析器来处理。
Some languages and language implementations allow a program to contain constructs called pragmas that provide directives or hints to the compiler. Pragmas that do not change program semantics—only the compilation process—are sometimes called significant comments. In some languages the name is also appropriate because, like comments, pragmas can appear anywhere in the source program. In this case they are usually processed by the scanner: allowing them anywhere in the grammar would greatly complicate the parser. In most languages, however, pragmas are permitted only at certain well-defined places in the grammar. In this case they are best processed by the parser or semantic analyzer.
作为指令的语句可能
Pragmas that serve as directives may
■ Turn various kinds of run-time checks (e.g., pointer or subscript checking) on or off
■ 打开或关闭某些代码改进(例如,在内部循环中打开以提高性能;关闭以提高编译速度)
■ Turn certain code improvements on or off (e.g., on in inner loops to improve performance; off otherwise to improve compilation speed)
■ 启用或禁用性能分析(收集统计信息以识别程序瓶颈)
■ Enable or disable performance profiling (statistics gathering to identify program bottlenecks)
一些指令“越界”并改变程序语义。例如,在 Ada 中,unchecked指令可用于禁用类型检查。在 OpenMP(我们将在第 13 章中讨论)中,指令指定了 Fortran、C 和 C++ 的重要并行扩展:创建、调度和同步线程。在这种情况下,将扩展表示为指令而不是更深入的集成更改的主要原因是明确划分核心语言和扩展之间的界限,并跨语言共享一组通用的扩展。
Some directives “cross the line” and change program semantics. In Ada, for example, the unchecked pragma can be used to disable type checking. In OpenMP, which we will consider in Chapter 13, pragmas specify significant parallel extensions to Fortran, C and C++: creating, scheduling, and synchronizing threads. In this case the principal rationale for expressing the extensions as pragmas rather than more deeply integrated changes is to sharply delineate the boundary between the core language and the extensions, and to share a common set of extensions across languages.
用作提示的指令为编译器提供了有关源程序的信息,可能使它能够更好地完成工作:
Pragmas that serve (merely) as hints provide the compiler with information about the source program that may allow it to do a better job:
■ 变量x使用非常频繁(将其保存在寄存器中可能是一个好主意)。
■ Variable x is very heavily used (it may be a good idea to keep it in a register).
■ 子程序F是一个纯函数:它对程序其余部分的唯一影响是它返回的值。
■ Subroutine F is a pure function: its only effect on the rest of the program is the value it returns.
■ 子程序S不是(间接)递归的(其存储可能是静态分配的)。
■ Subroutine S is not (indirectly) recursive (its storage may be statically allocated).
■ 对于浮点变量x来说,32 位精度(而不是 64 位)就足够了。
■ 32 bits of precision (instead of 64) suffice for floating-point variable x.
编译器可能会为了简单起见或者面对矛盾的信息而忽略这些。
The compiler may ignore these in the interest of simplicity, or in the face of contradictory information.
C++11 中引入了编译指示的标准语法(它们被称为“属性”)。例如,可以将一个打印错误消息并终止执行的函数标记为 [[ noreturn ]],以允许编译器优化围绕调用的代码,或者发出更有帮助的错误或警告消息。截至撰写本文时,供应商可以扩展支持的属性集(通过修改编译器),但普通程序员不能扩展。这些属性应限于提示(而不是指令)的程度一直存在争议。Java(称为“批注”)和 C#(称为“属性”)中的新编译指示可由程序员定义;我们将在第 16.3.1 节中返回讨论这些内容。
Standard syntax for pragmas was introduced in C++11 (where they are known as “attributes”). A function that prints an error message and terminates execution, for example, can be labeled [[noreturn]], to allow the compiler to optimize code around calls, or to issue more helpful error or warning messages. As of this writing, the set of supported attributes can be extended by vendors (by modifying the compiler), but not by ordinary programmers. The extent to which these attributes should be limited to hints (rather than directives) has been somewhat controversial. New pragmas in Java (which calls them “annotations”) and C# (which calls them “attributes”) can be defined by the programmer; we will return to these in Section 16.3.1.
解析器是典型编译器的核心。它调用扫描器来获取输入程序的标记,将标记组合成语法树,并将该树(可能一次一个子例程)传递给编译器的后续阶段,执行语义分析和代码生成和改进。实际上,解析器“负责”整个编译过程;这种编译风格有时被称为语法制导翻译。
The parser is the heart of a typical compiler. It calls the scanner to obtain the tokens of the input program, assembles the tokens together into a syntax tree, and passes the tree (perhaps one subroutine at a time) to the later phases of the compiler, which perform semantic analysis and code generation and improvement. In effect, the parser is “in charge” of the entire compilation process; this style of compilation is sometimes referred to as syntax-directed translation.
如本章简介中所述,上下文无关文法 (CFG) 是CF 语言的生成器。解析器是一种语言识别器。可以证明,对于任何 CFG,我们都可以创建一个运行时间为O ( n3 ) 的解析器,其中n是输入程序的长度。12有两种著名的解析算法可以达到这个界限:Earley 算法 [ Ear70 ] 和 Cocke-Younger-Kasami (CYK) 算法 [ Kas65、You67 ]。立方时间对于解析大型程序来说太慢了,但幸运的是,并非所有语法都需要这种通用且缓慢的解析算法。我们可以为很多类语法构建以线性时间运行的解析器。其中最重要的两个类称为 LL 和 LR(图 2.13)。
As noted in the introduction to this chapter, a context-free grammar (CFG) is a generator for a CF language. A parser is a language recognizer. It can be shown that for any CFG we can create a parser that runs in O(n3) time, where n is the length of the input program.12 There are two well-known parsing algorithms that achieve this bound: Earley's algorithm [Ear70] and the Cocke-Younger-Kasami (CYK) algorithm [Kas65, You67]. Cubic time is much too slow for parsing sizable programs, but fortunately not all grammars require such a general and slow parsing algorithm. There are large classes of grammars for which we can build parsers that run in linear time. The two most important of these classes are called LL and LR (Figure 2.13).
LL 代表“从左到右,最左派生”。LR 代表“从左到右,最右派生”。在这两个类中,输入都是从左到右读取的,并且解析器尝试发现(构造)该输入的派生。对于 LL 解析器,派生将是最左边的;对于 LR 解析器,派生将是最右边的。我们将首先介绍 LL 解析器。它们通常被认为更简单且更容易理解。它们可以手写,也可以由解析器生成工具从适当的语法自动生成。LR 语法类更大(即,LR 语法比 LL 语法更多),有些人发现 LR 语法的结构更直观,尤其是在处理算术表达式时。LR 解析器几乎总是由解析器生成工具构建。这两类解析器都用于生产编译器,尽管 LR 解析器更常见。
LL stands for “Left-to-right, Left-most derivation.” LR stands for “Left-to-right, Right-most derivation.” In both classes the input is read left-to-right, and the parser attempts to discover (construct) a derivation of that input. For LL parsers, the derivation will be left-most; for LR parsers, right-most. We will cover LL parsers first. They are generally considered to be simpler and easier to understand. They can be written by hand or generated automatically from an appropriate grammar by a parser-generating tool. The class of LR grammars is larger (i.e., more grammars are LR than LL), and some people find the structure of the LR grammars more intuitive, especially in the handling of arithmetic expressions. LR parsers are almost always constructed by a parser-generating tool. Both classes of parsers are used in production compilers, though LR parsers are more common.
LL 解析器也称为“自上而下”或“预测”解析器。它们从根部向下构建解析树,在每个步骤中根据下一个可用的输入标记预测将使用哪个生成式来扩展当前节点。LR 解析器也称为“自下而上”解析器。它们从叶子向上构建解析树,识别何时可以将一组叶子或其他节点连接在一起作为单个父节点的子节点。
LL parsers are also called “top-down,” or “predictive” parsers. They construct a parse tree from the root down, predicting at each step which production will be used to expand the current node, based on the next available token of input. LR parsers are also called “bottom-up” parsers. They construct a parse tree from the leaves up, recognizing when a collection of leaves or other nodes can be joined together as the children of a single parent.
LR 解析器有几个重要的子类,包括 SLR、LALR 和“完整 LR”。SLR 和 LALR 因其易于实现而重要,而完整 LR 因其通用性而重要。LL 解析器也可以分为 SLL 和“完整 LL”子类。我们在此仅简要介绍它们之间的差异;有关更多信息,请参阅任何标准编译器构造或解析理论教科书 [ App97、ALSU07、AU72、CT04、FCL10、GBJ + 12 ]。
There are several important subclasses of LR parsers, including SLR, LALR, and “full LR.” SLR and LALR are important for their ease of implementation, full LR for its generality. LL parsers can also be grouped into SLL and “full LL” subclasses. We will cover the differences among them only briefly here; for further information see any of the standard compiler-construction or parsing theory textbooks [App97, ALSU07, AU72, CT04, FCL10, GBJ+12].
我们通常看到 LL 或 LR(或其他名称)后面带有括号中的数字:例如 LL(2) 或 LALR(1)。此数字表示解析需要多少个前瞻标记。大多数实际编译器只使用一个前瞻标记,但有时使用更多标记会有所帮助。特别是,开源工具 ANTLR 使用多标记前瞻来扩大适合自上而下解析的语言类 [ PQ95 ]。在第 2.3.1 节中,我们将更详细地介绍 LL(1) 语法和手写解析器。在第 2.3.3 节和2.3.4节中,我们将考虑自动生成的 LL(1) 和 LR(1)(实际上是 SLR(1))解析器。
One commonly sees LL or LR (or whatever) written with a number in parentheses after it: LL(2) or LALR(1), for example. This number indicates how many tokens of look-ahead are required in order to parse. Most real compilers use just one token of look-ahead, though more can sometimes be helpful. The open-source ANTLR tool, in particular, uses multitoken look-ahead to enlarge the class of languages amenable to top-down parsing [PQ95]. In Section 2.3.1 we will look at LL(1) grammars and handwritten parsers in more detail. In Sections 2.3.3 and 2.3.4 we will consider automatically generated LL(1) and LR(1) (actually SLR(1)) parsers.
那么,我们如何使用计算器语法来解析字符串呢?我们在图 2.14中看到了基本思想。我们从树的顶部开始,根据树中当前最左边的非终结符和当前输入标记预测所需的产生式。我们可以通过两种方式之一来形式化此过程。第一种方法(本小节的其余部分将介绍)是构建一个递归下降解析器,其子例程与语法的非终结符一一对应。递归下降解析器通常是手工构建的,尽管 ANTLR 解析器生成器会根据输入语法自动构建它们。第二种方法(第2.3.3 节中介绍)是构建一个LL 解析表,然后由驱动程序读取。表驱动解析器几乎总是由解析器生成器自动构建。这两个选项(递归下降和表驱动)让人想起嵌套的case语句和表驱动方法构建我们在2.2.2和2.2.3节中看到的扫描器。需要强调的是,它们实现了相同的基本解析算法。
So how do we parse a string with our calculator grammar? We saw the basic idea in Figure 2.14. We start at the top of the tree and predict needed productions on the basis of the current left-most nonterminal in the tree and the current input token. We can formalize this process in one of two ways. The first, described in the remainder of this subsection, is to build a recursive descent parser whose subroutines correspond, one-one, to the nonterminals of the grammar. Recursive descent parsers are typically constructed by hand, though the ANTLR parser generator constructs them automatically from an input grammar. The second approach, described in Section 2.3.3, is to build an LL parse table which is then read by a driver program. Table-driven parsers are almost always constructed automatically by a parser generator. These two options—recursive descent and table-driven—are reminiscent of the nested case statements and table-driven approaches to building a scanner that we saw in Sections 2.2.2 and 2.2.3. It should be emphasized that they implement the same basic parsing algorithm.
当要解析的语言相对简单,或者没有可用的解析器生成器工具时,最常使用手写递归下降解析器。但是也有例外。特别是,递归下降出现在 GNU 编译器集合 ( gcc ) 的最新版本中。早期版本使用bison自动创建自下而上的解析器。进行此更改部分是出于性能原因,部分是为了能够生成更高质量的语法错误消息。(bison代码更容易编写,而且可以说更易于维护。)
Handwritten recursive descent parsers are most often used when the language to be parsed is relatively simple, or when a parser-generator tool is not available. There are exceptions, however. In particular, recursive descent appears in recent versions of the GNU compiler collection (gcc). Earlier versions used bison to create a bottom-up parser automatically. The change was made in part for performance reasons and in part to enable the generation of higher-quality syntax error messages. (The bison code was easier to write, and arguably easier to maintain.)
如果没有附加代码(图 2.17中未显示),解析器仅验证程序在语法上是否正确(即,case语句中的parse_error子句均未执行,并且match始终看到它期望看到的内容)。为了对编译器的其余部分有用(必须用其他语言生成等效的目标程序),解析器必须将解析树或程序片段的其他表示保存为显式数据结构。要保存解析树本身,我们可以在执行表示这些子节点的递归子例程和匹配调用之前立即分配并链接记录以表示节点的子节点。我们需要向每个递归例程传递一个参数,该参数指向要扩展的记录(即要发现其子节点)。过程匹配还需要在树的叶子中保存有关某些标记(例如,标识符和文字的字符串表示)的信息。
Without additional code (not shown in Figure 2.17), the parser merely verifies that the program is syntactically correct (i.e., that none of the otherwise parse_error clauses in the case statements are executed and that match always sees what it expects to see). To be of use to the rest of the compiler—which must produce an equivalent target program in some other language—the parser must save the parse tree or some other representation of program fragments as an explicit data structure. To save the parse tree itself, we can allocate and link together records to represent the children of a node immediately before executing the recursive subroutines and match invocations that represent those children. We shall need to pass each recursive routine an argument that points to the record that is to be expanded (i.e., whose children are to be discovered). Procedure match will also need to save information about certain tokens (e.g., character-string representations of identifiers and literals) in the leaves of the tree.
正如我们在第 1 章中看到的,解析树包含大量无关细节,这些细节无需为编译器的其余部分保存。因此,解析器很少显式地构造完整的解析树。它更经常生成抽象语法树或其他更简洁的表示。在递归下降编译器中,可以通过仅在递归调用的子集中分配和链接记录来创建语法树。
As we saw in Chapter 1, the parse tree contains a great deal of irrelevant detail that need not be saved for the rest of the compiler. It is therefore rare for a parser to construct a full parse tree explicitly. More often it produces an abstract syntax tree or some other more terse representation. In a recursive descent compiler, a syntax tree can be created by allocating and linking together records in only a subset of the recursive calls.
编写递归下降解析器最棘手的部分是找出应该用哪些标记来标记case语句的分支。每个分支代表一个产生式:即子程序所对应符号的一种可能的展开。标记X 可以预测产生式,原因有二:(1)产生式的右侧在递归展开时,可能产生一个以X开头的字符串,或 (2) 右侧可能不产生任何结果(即,它是ε ,或者是可以递归产生ε的非终结符字符串),并且X可能开始产生接下来的结果。我们将在第 2.3.3 节中使用称为 FIRST 和 FOLLOW 的集合来形式化这个预测概念,并展示如何从 LL(1) CFG 自动推导它们。
The trickiest part of writing a recursive descent parser is figuring out which tokens should label the arms of the case statements. Each arm represents one production: one possible expansion of the symbol for which the subroutine was named. The tokens that label a given arm are those that predict the production. A token X may predict a production for either of two reasons: (1) the right-hand side of the production, when recursively expanded, may yield a string beginning with X, or (2) the right-hand side may yield nothing (i.e., it is ε, or a string of nonterminals that may recursively yield ε), and X may begin the yield of what comes next. We will formalize this notion of prediction in Section 2.3.3, using sets called FIRST and FOLLOW, and show how to derive them automatically from an LL(1) CFG.
在设计递归下降解析器时,必须掌握编写和修改 LL(1) 语法的一定能力。“LL(1) 性”最常见的两个障碍是左递归和公共前缀。
When designing a recursive-descent parser, one has to acquire a certain facility in writing and modifying LL(1) grammars. The two most common obstacles to “LL(1)-ness” are left recursion and common prefixes.
左递归和公共前缀都可以从语法中机械地删除。一般情况有点棘手(练习 2.25),因为预测问题可能是间接问题(例如,S → A α和 A → S β,或S → A α,S → B β,A ⇒ * c γ,和B ⇒ * c δ)。不过,我们可以在上面的例子中看到一般的想法。
Both left recursion and common prefixes can be removed from a grammar mechanically. The general case is a little tricky (Exercise 2.25), because the prediction problem maybe an indirect one (e.g., S → A α and A → S β, or S → A α, S → B β, A ⇒* c γ, and B ⇒* c δ). We can see the general idea in the examples above, however.
当然,仅仅消除左递归和公共前缀并不能保证使语法成为 LL(1)。有无数种非 LL语言(不存在 LL 语法的语言),消除左递归和公共前缀的机械转换对它们的语法非常有效。幸运的是,实践中出现的少数非 LL 语言通常可以通过用一两个简单的启发式方法增强解析算法来处理。
Of course, simply eliminating left recursion and common prefixes is not guaranteed to make a grammar LL(1). There are infinitely many non-LL languages—languages for which no LL grammar exists—and the mechanical transformations to eliminate left recursion and common prefixes work on their grammars just fine. Fortunately, the few non-LL languages that arise in practice can generally be handled by augmenting the parsing algorithm with one or two simple heuristics.
无论是自上而下还是自下而上解析,通常的方法都是将歧义语法与“消歧规则”结合使用,该规则规定,如果两个可能的产生式发生冲突,则使用在语法中文本上最先出现的那个。在上面的歧义片段中,else_clause → else stmt位于else_clause → ε之前,这一事实最终将else与最近的then配对。
The usual approach, whether parsing top-down or bottom-up, is to use the ambiguous grammar together with a “disambiguating rule,” which says that in the case of a conflict between two possible productions, the one to use is the one that occurs first, textually, in the grammar. In the ambiguous fragment above, the fact that else_clause → else stmt comes before else_clause → ε ends up pairing the else with the nearest then.
Modula-2 使用END来终止其所有结构化语句。Ada 和 Fortran 77 使用 end if来结束 if(使用end w hile来结束while,等等)。Algol 68 通过反向拼写初始关键字来创建其终止符(if…fi、case…esac、do…od,等等)。
Modula-2 uses END to terminate all its structured statements. Ada and Fortran 77 end an if with end if (and a while with end while, etc.). Algol 68 creates its terminators by spelling the initial keyword backward (if… fi, case… esac, do… od, etc.).
正如我们在第 2.3.1 节末尾所暗示的,预测集是用称为 FIRST 和 FOLLOW 的更简单的集合来定义的,其中 FIRST( A ) 是所有可以作为A开头的标记的集合,而 FOLLOW( A ) 是某个有效程序中可以跟在A之后的所有标记的集合。如果我们以显而易见的方式扩展 FIRST 的定义域以包括符号字符串,那么我们说产生式A → β的预测集是 FIRST ( β ),加上如果β ⇒ * ε 的FOLLOW( A ) 。为了符号方便,我们定义谓词 EPS 使得 EPS( β ) ≡ β ⇒ * ε。
As we hinted at the end of Section 2.3.1, predict sets are defined in terms of simpler sets called FIRST and FOLLOW, where FIRST(A) is the set of all tokens that could be the start of an A and FOLLOW(A) is the set of all tokens that could come after an A in some valid program. If we extend the domain of FIRST in the obvious way to include strings of symbols, we then say that the predict set of a production A → β is FIRST (β), plus FOLLOW(A) if β ⇒* ε. For notational convenience, we define the predicate EPS such that EPS(β) ≡ β ⇒* ε.
图 2.24中更正式地给出了计算 EPS、FIRST、FOLLOW 和 PREDICT 集的算法。它依赖于以下定义:
The algorithm to compute EPS, FIRST, FOLLOW, and PREDICT sets appears, a bit more formally, in Figure 2.24. It relies on the following definitions:
EPS(α) ≡ if α ⇒* ε then true else false
FIRST( α ) ≡ { c : α⇒ * cβ }
FIRST(α) ≡ {c : α ⇒* c β }
FOLLOW( A ) ≡ {c : S ⇒ + α A c β }
FOLLOW(A) ≡ {c : S ⇒+ α A c β }
PREDICT( A → α ) ≡ FIRST( α ) ∪ (如果 EPS( α ) 则 FOLLOW( A ) 否则 ∅)
PREDICT(A → α) ≡ FIRST(α) ∪ ( if EPS(α) then FOLLOW(A) else ∅)
PREDICT 的定义假设该语言已经增加了结束标记,即 FOLLOW( S ) = {$$}。请注意,长度大于 1 的字符串的 FIRST 集和 EPS 值是根据需要计算的;它们是不显式存储。算法保证终止(即收敛到一个解决方案),因为 FIRST 和 FOLLOW 集的大小受语法中终端数量的限制。
The definition of PREDICT assumes that the language has been augmented with an end marker—that is, that FOLLOW(S) = {$$}. Note that FIRST sets and EPS values for strings of length greater than one are calculated on demand; they are not stored explicitly. The algorithm is guaranteed to terminate (i.e., converge on a solution), because the sizes of the FIRST and FOLLOW sets are bounded by the number of terminals in the grammar.
如果在计算 PREDICT 集的过程中,我们发现某个标记属于多个具有相同左侧的产生式的 PREDICT 集,则该语法不是 LL(1),因为当左侧位于解析堆栈的顶部(或者我们在递归下降解析器中左侧的子例程中)并且我们看到该标记出现在输入中时,我们将无法选择使用哪个产生式。这种歧义称为预测-预测冲突;它可能是因为同一个标记可以开始多个右侧,或者因为它可以开始一个右侧并且也可以出现在某个有效程序的左侧之后,并且一个可能的右侧可以生成ε而产生的。
If in the process of calculating PREDICT sets we find that some token belongs to the PREDICT set of more than one production with the same left-hand side, then the grammar is not LL(1), because we will not be able to choose which of the productions to employ when the left-hand side is at the top of the parse stack (or we are in the left-hand side's subroutine in a recursive descent parser) and we see the token coming up in the input. This sort of ambiguity is known as a predict-predict conflict; it can arise either because the same token can begin more than one right-hand side, or because it can begin one right-hand side and can also appear after the left-hand side in some valid program, and one possible right-hand side can generate ε.
从概念上讲,正如我们在2.3 节开头看到的那样,自下而上的解析器通过维护解析树的部分完成的子树森林来工作,每当它识别出输入字符串最右侧派生中使用的某些产生式右侧的符号时,它就会将这些子树连接在一起。它会创建一个新的内部节点,并将连接在一起的树的根节点作为该节点的子节点。
Conceptually, as we saw at the beginning of Section 2.3, a bottom-up parser works by maintaining a forest of partially completed subtrees of the parse tree, which it joins together whenever it recognizes the symbols on the right-hand side of some production used in the right-most derivation of the input string. It creates a new internal node and makes the roots of the joined-together trees the children of that node.
实际上,自下而上的解析器几乎总是表驱动的。它将部分完成的子树的根保存在堆栈中。当它从扫描器将标记移入堆栈。当它识别出堆栈顶部的几个符号构成右侧时,它会通过弹出堆栈并将左侧推入其位置来将这些符号缩减到其左侧。堆栈的作用是自上而下和自下而上解析之间的第一个重要区别:自上而下的解析器的堆栈包含解析器期望在未来看到的内容的列表;自下而上的解析器的堆栈包含解析器过去已经看到的内容的记录。
In practice, a bottom-up parser is almost always table-driven. It keeps the roots of its partially completed subtrees on a stack. When it accepts a new token from the scanner, it shifts the token into the stack. When it recognizes that the top few symbols on the stack constitute a right-hand side, it reduces those symbols to their left-hand side by popping them off the stack and pushing the left-hand side in their place. The role of the stack is the first important difference between top-down and bottom-up parsing: a top-down parser's stack contains a list of what the parser expects to see in the future; a bottom-up parser's stack contains a record of what the parser has already seen in the past.
LR 系列解析器通过将已遍历的状态与语法符号一起推送到解析堆栈中来跟踪它们。事实上,驱动解析算法的是状态(而不是符号):它们告诉我们在右侧开始时我们处于什么状态。具体来说,当状态和输入的组合告诉我们需要使用产生式A → α进行归约时,我们会从堆栈中弹出长度(α)个符号,以及我们移动的状态记录在移动这些符号时,这些弹出窗口会显示我们在移动之前的状态,这样我们就可以返回到该状态并继续操作,就像我们一开始就看到了A一样。
An LR-family parser keeps track of the states it has traversed by pushing them into the parse stack, along with the grammar symbols. It is in fact the states (rather than the symbols) that drive the parsing algorithm: they tell us what state we were in at the beginning of a right-hand side. Specifically, when the combination of state and input tells us we need to reduce using production A → α, we pop length(α) symbols off the stack, together with the record of states we moved through while shifting those symbols. These pops expose the state we were in immediately prior to the shifts, allowing us to return to that state and proceed as if we had seen A in the first place.
我们可以将 LR 系列解析器的移位规则视为有限自动机的转换函数,与我们用来建模扫描仪的自动机非常相似。自动机的每个状态都对应于一个项目列表,这些项目指示解析器在解析过程中的某个特定点可能处于的位置。输入符号X(可能是终结符或非终结符)的转换移动到一个状态,该状态的基础由右侧的X上的 • 已移动的项目以及需要添加为闭包的任何项目组成。这些列表由自下而上的解析器生成器构建,以构建自动机,但在解析过程中不需要。
We can think of the shift rules of an LR-family parser as the transition function of a finite automaton, much like the automata we used to model scanners. Each state of the automaton corresponds to a list of items that indicate where the parser might be at some specific point in the parse. The transition for input symbol X (which may be either a terminal or a nonterminal) moves to a state whose basis consists of items in which the • has been moved across an X in the right-hand side, plus whatever items need to be added as closure. The lists are constructed by a bottom-up parser generator in order to build the automaton, but are not needed during parsing.
事实证明,LR 系列解析器中较简单的成员 LR(0)、SLR(1) 和 LALR(1) 都使用相同的自动机,称为特征有限状态机(CFSM)。完整的 LR 解析器使用具有(对于大多数语法)更多状态的机器。这些算法之间的差异在于它们如何处理包含移位-归约冲突的状态- 一个项的 • 位于终结符前面(表明需要移位),另一个项的 • 位于右侧末尾(表明需要归约)。LR(0) 解析器仅在没有这样的状态时工作。可以证明,通过添加结束标记(即$$),任何可以自下而上确定性解析的语言都具有 LR(0) 语法。不幸的是,实际编程语言的 LR(0) 语法往往非常大且不直观。
It turns out that the simpler members of the LR family of parsers—LR(0), SLR(1), and LALR(1)—all use the same automaton, called the characteristic finite-state machine, or CFSM. Full LR parsers use a machine with (for most grammars) a much larger number of states. The differences between the algorithms lie in how they deal with states that contain a shift-reduce conflict—one item with the • in front of a terminal (suggesting the need for a shift) and another with the • at the end of the right-hand side (suggesting the need for a reduction). An LR(0) parser works only when there are no such states. It can be proven that with the addition of an end-marker (i.e., $$), any language that can be deterministically parsed bottom-up has an LR(0) grammar. Unfortunately, the LR(0) grammars for real programming languages tend to be prohibitively large and unintuitive.
SLR(简单 LR)解析器会查看即将到来的输入并使用 FOLLOW 集来解决冲突。仅当即将到来的标记在 FOLLOW( α ) 中时,SLR 解析器才会通过A → α调用约简。但是,如果标记也位于状态其他项中 • 后面的任何符号的 FIRST 集中,则它仍然会看到冲突。事实证明,在重要的情况下,标记可能在有效程序的某个地方跟随给定的非终结符,但永远不会在当前状态描述的上下文中出现。对于这些情况,全局 FOLLOW 集过于粗糙。LALR(前瞻 LR)解析器通过使用局部(特定于状态的)前瞻来改进 SLR。
SLR (simple LR) parsers peek at upcoming input and use FOLLOW sets to resolve conflicts. An SLR parser will call for a reduction via A → α only if the upcoming token(s) are in FOLLOW(α). It will still see a conflict, however, if the tokens are also in the FIRST set of any of the symbols that follow a • in other items of the state. As it turns out, there are important cases in which a token may follow a given nonterminal somewhere in a valid program, but never in a context described by the current state. For these cases global FOLLOW sets are too crude. LALR (look-ahead LR) parsers improve on SLR by using local (state-specific) look-ahead instead.
当同一组项目可能出现在 CFSM 中的两条不同路径上时,LALR 解析器中仍会出现冲突。两条路径最终都会处于同一状态,此时特定于状态的前瞻无法再区分它们。完整的 LR 解析器会复制状态,以便在它们的本地前瞻不同时保持路径不相交。
Conflicts can still arise in an LALR parser when the same set of items can occur on two different paths through the CFSM. Both paths will end up in the same state, at which point state-specific look-ahead can no longer distinguish between them. A full LR parser duplicates states in order to keep paths disjoint when their local look-aheads are different.
LALR 解析器是实践中最常见的自下而上的解析器。它们的大小和速度与 SLR 解析器相同,但能够解决更多冲突。实际编程语言的完整 LR 解析器往往非常大。一些研究人员已经开发出减少完整 LR 表大小的技术,但 LALR 在实践中运行良好,通常不需要完整 LR 的额外复杂性。Yacc /bison为 LALR 解析器生成 C 代码。
LALR parsers are the most common bottom-up parsers in practice. They are the same size and speed as SLR parsers, but are able to resolve more conflicts. Full LR parsers for real programming languages tend to be very large. Several researchers have developed techniques to reduce the size of full-LR tables, but LALR works sufficiently well in practice that the extra complexity of full LR is usually not required. Yacc/bison produces C code for an LALR parser.
与表驱动的 LL(1) 解析器一样,SLR(1)、LALR(1) 或 LR(1) 解析器执行循环,在该循环中反复检查二维表以找出要采取的操作。但是,LR 系列解析器不使用当前输入标记和堆栈顶部的非终结符来索引表,而是使用当前输入标记和当前解析器状态(可在堆栈顶部找到)。“Shift”表条目表示应推送的状态。“Reduce”表条目表示应弹出的状态数和应推回到输入流中的非终结符,以便按弹出未覆盖的状态进行移动。对于减少产生式右侧的每个符号,始终有一个弹出状态。可以使用未覆盖的状态和新识别的非终结符索引表来找到下一个要推送的状态。
Like a table-driven LL(1) parser, an SLR(1), LALR(1), or LR(1) parser executes a loop in which it repeatedly inspects a two-dimensional table to find out what action to take. However, instead of using the current input token and top-of-stack nonterminal to index into the table, an LR-family parser uses the current input token and the current parser state (which can be found at the top of the stack). “Shift” table entries indicate the state that should be pushed. “Reduce” table entries indicate the number of states that should be popped and the nonterminal that should be pushed back onto the input stream, to be shifted by the state uncovered by the pops. There is always one popped state for every symbol on the right-hand side of the reducing production. The state to be pushed next can be found by indexing into the table using the uncovered state and the newly recognized nonterminal.
请注意,通常情况下,语句列表为空是有意义的。在计算器语言中,它只允许一个空程序,这确实很愚蠢。然而,在实际语言中,它允许结构化语句的主体为空,这可能非常有用。人们经常希望 case或多路if…then…else语句的一个分支为空,而空的while循环允许并行程序(或操作系统)等待来自另一个进程或 I/O 设备的信号。
Note that it does in general make sense to have an empty statement list. In the calculator language it simply permits an empty program, which is admittedly silly. In real languages, however, it allows the body of a structured statement to be empty, which can be very useful. One frequently wants one arm of a case or multi-way if… then … else statement to be empty, and an empty while loop allows a parallel program (or the operating system) to wait for a signal from another process or an I/O device.
一般来说,语法错误恢复这一术语适用于任何允许编译器在遇到语法错误时继续在程序后面查找其他错误的技术。高质量的语法错误恢复对于任何生产质量的编译器都是必不可少的。恢复技术越好,编译器就越有可能正确识别其他错误(尤其是附近的错误),并且越不可能在程序后面混淆并报告虚假的级联错误。
In general, the term syntax error recovery is applied to any technique that allows the compiler, in the face of a syntax error, to continue looking for other errors later in the program. High-quality syntax error recovery is essential in any production-quality compiler. The better the recovery technique, the more likely the compiler will be to recognize additional errors (especially nearby errors) correctly, and the less likely it will be to become confused and announce spurious cascading errors later in the program.
更深入地
IN MORE DEPTH
在配套网站上,我们探讨了几种可能的语法错误恢复方法。在恐慌模式下,编译器编写者定义了一小组“安全符号”,用于界定输入中的干净点。分号通常用于结束语句,在许多语言中都是不错的选择。当发生错误时,编译器会删除输入标记,直到找到安全符号,然后“退出解析器”(例如,从递归下降子例程返回),直到找到可能出现该符号的上下文。短语级恢复通过在不同的语法产生式中使用不同的“安全”符号集(在表达式中时使用右括号;在声明中时使用分号)来改进这种技术。上下文特定的前瞻通过区分给定产生式可能出现在不同上下文中来获得额外的改进语法树。为了妥善应对某些常见的编程错误,编译器编写者可能会在语法中添加错误产生式,以捕获不正确但经常被错误编写的语言特定习语。
On the companion site we explore several possible approaches to syntax error recovery. In panic mode, the compiler writer defines a small set of “safe symbols” that delimit clean points in the input. Semicolons, which typically end a statement, are a good choice in many languages. When an error occurs, the compiler deletes input tokens until it finds a safe symbol, and then “backs the parser out” (e.g., returns from recursive descent subroutines) until it finds a context in which that symbol might appear. Phrase-level recovery improves on this technique by employing different sets of “safe” symbols in different productions of the grammar (right parentheses when in an expression; semicolons when in a declaration). Context-specific look-ahead obtains additional improvements by differentiating among the various contexts in which a given production might appear in a syntax tree. To respond gracefully to certain common programming errors, the compiler writer may augment the grammar with error productions that capture language-specific idioms that are incorrect but are often written by mistake.
Niklaus Wirth 于 1976 年发表了递归下降解析器的短语级和上下文特定恢复的优雅实现 [ Wir76,第 5.9 节]。如果编译器所用的语言支持异常(将在第 9.4 节中进一步讨论),则异常提供了一种更简单的替代方案。对于表驱动的自上而下的解析器,Fischer、Milton 和 Quiring 于 1980 年发表了一种算法,该算法自动实现了明确定义的局部最小成本语法修复概念。局部最小成本修复也可以在自下而上的解析器中实现,但难度要大得多。大多数自下而上的解析器依赖于更直接的短语级恢复;典型示例可在yacc/bison中找到。
Niklaus Wirth published an elegant implementation of phrase-level and context-specific recovery for recursive descent parsers in 1976 [Wir76, Sec. 5.9]. Exceptions (to be discussed further in Section 9.4) provide a simpler alternative if supported by the language in which the compiler is written. For table-driven top-down parsers, Fischer, Milton, and Quiring published an algorithm in 1980 that automatically implements a well-defined notion of locally least-cost syntax repair. Locally least-cost repair is also possible in bottom-up parsers, but it is significantly more difficult. Most bottom-up parsers rely on more straightforward phrase-level recovery; a typical example can be found in yacc/bison.
我们对扫描器、解析器、正则表达式和上下文无关语法的相对角色和计算能力的理解基于自动机理论的形式化。在自动机理论中,形式语言是从有限字母表中抽取的一组符号串。形式语言可以通过生成语言的一组规则(如正则表达式或上下文无关语法)来指定,也可以通过接受(识别)该语言的形式机器来指定。形式机器将符号串作为输入,输出“是”或“否”。如果机器对某种语言中的所有字符串(且只对其中某些字符串)都说“是”,则称机器接受该语言。或者,语言也可以定义为某台机器说“是”的字符串集合。
Our understanding of the relative roles and computational power of scanners, parsers, regular expressions, and context-free grammars is based on the formalisms of automata theory. In automata theory, a formal language is a set of strings of symbols drawn from a finite alphabet. A formal language can be specified either by a set of rules (such as regular expressions or a context-free grammar) that generates the language, or by a formal machine that accepts (recognizes) the language. A formal machine takes strings of symbols as input and outputs either “yes” or “no.” A machine is said to accept a language if it says “yes” to all and only those strings that are in the language. Alternatively, a language can be defined as the set of strings for which a particular machine says “yes.”
形式语言可以分为一系列逐渐增大的类别,称为乔姆斯基层次结构。14大多数类别可以用两种方式来表征:可通过可用于生成字符串集的规则类型来表征,或可通过能够识别该语言的形式机器类型来表征。如我们所见,正则语言是使用连接、交替和 Kleene 闭包来定义的,并可由扫描器识别。上下文无关语言是正则语言的真正超集。它们可通过连接、交替和递归(包含 Kleene 闭包)来定义,并可由解析器识别。扫描器是有限自动机(一种形式机器)的具体实现。解析器是下推自动机的具体实现。正如上下文无关语法为正则表达式添加递归一样,下推自动机为有限自动机的内存添加了堆栈。乔姆斯基层次结构中还有其他级别,但它们与编译器构造的直接适用性较差,因此本文不再赘述。
Formal languages can be grouped into a series of successively larger classes known as the Chomsky hierarchy.14 Most of the classes can be characterized in two ways: by the types of rules that can be used to generate the set of strings, or by the type of formal machine that is capable of recognizing the language. As we have seen, regular languages are defined by using concatenation, alternation, and Kleene closure, and are recognized by a scanner. Context-free languages are a proper superset of the regular languages. They are defined by using concatenation, alternation, and recursion (which subsumes Kleene closure), and are recognized by a parser. A scanner is a concrete realization of a finite automaton, a type of formal machine. A parser is a concrete realization of a push-down automaton. Just as context-free grammars add recursion to regular expressions, push-down automata add a stack to the memory of a finite automaton. There are additional levels in the Chomsky hierarchy, but they are less directly applicable to compiler construction, and are not covered here.
可以建设性地证明,正则表达式和有限自动机是等价的:可以构造一个有限自动机,它接受由给定正则表达式定义的语言,反之亦然。类似地,可以构造一个下推自动机,它接受由给定上下文无关语法定义的语言,反之亦然。语法到自动机的构造实际上是由扫描器和解析器生成器(例如lex和yacc )执行的。当然,真正的扫描器不会只接受一个标记;它会在循环中被调用,以便不断重复接受标记。如边栏 2.4 中所述,通过让扫描器接受语言中所有标记的交替(具有不同的最终状态),并让它继续使用字符,直到无法再构造标记,来实现这个细节。
It can be proven, constructively, that regular expressions and finite automata are equivalent: one can construct a finite automaton that accepts the language defined by a given regular expression, and vice versa. Similarly, it is possible to construct a push-down automaton that accepts the language defined by a given context-free grammar, and vice versa. The grammar-to-automaton constructions are in fact performed by scanner and parser generators such as lex and yacc. Of course, a real scanner does not accept just one token; it is called in a loop so that it keeps accepting tokens repeatedly. As noted in Sidebar 2.4, this detail is accommodated by having the scanner accept the alternation of all the tokens in the language (with distinguished final states), and by having it continue to consume characters until no longer token can be constructed.
更深入地
IN MORE DEPTH
在配套网站上,我们更详细地讨论了有限自动机和下推自动机。我们给出了一个将 DFA 转换为等效正则表达式的算法。结合第2.2.1 节中的构造,该算法证明了正则表达式和有限自动机的等价性。我们还考虑了各种线性时间解析算法可以和不能解析的语法和语言集。
On the companion site we consider finite and pushdown automata in more detail. We give an algorithm to convert a DFA into an equivalent regular expression. Combined with the constructions in Section 2.2.1, this algorithm demonstrates the equivalence of regular expressions and finite automata. We also consider the sets of grammars and languages that can and cannot be parsed by the various linear-time parsing algorithms.
在本章中,我们介绍了正则表达式和上下文无关文法的形式化,以及实际编译器中扫描和解析的基础算法。我们还提到了语法错误恢复,并简要概述了自动机理论的相关部分。正则表达式和上下文无关文法是语言生成器:它们指定如何构造有效的字符串或标记。扫描器和解析器是语言识别器:它们指示给定的字符串是否有效。扫描器的主要工作是通过将字符分组为标记以及删除注释和空格来减少解析器必须处理的信息量。扫描器和解析器生成器会自动将正则表达式和上下文无关文法转换为扫描器和解析器。
In this chapter we have introduced the formalisms of regular expressions and context-free grammars, and the algorithms that underlie scanning and parsing in practical compilers. We also mentioned syntax error recovery, and presented a quick overview of relevant parts of automata theory. Regular expressions and context-free grammars are language generators: they specify how to construct valid strings of characters or tokens. Scanners and parsers are language recognizers: they indicate whether a given string is valid. The principal job of the scanner is to reduce the quantity of information that must be processed by the parser, by grouping characters together into tokens, and by removing comments and white space. Scanner and parser generators automatically translate regular expressions and context-free grammars into scanners and parsers.
编程语言的实用解析器(以线性时间运行的解析器)主要分为两类:自上而下(也称为 LL 或预测)和自下而上(也称为 LR 或移位归约)。自上而下的解析器从根开始构建解析树,然后从左到右进行深度优先遍历。自下而上的解析器从叶子开始构建解析树,同样从左到右进行,并在识别出内部节点的子节点时将部分树合并在一起。自上而下的解析器的堆栈包含对未来将看到的内容的预测;自下而上的解析器的堆栈包含过去看到的内容的记录。
Practical parsers for programming languages (parsers that run in linear time) fall into two principal groups: top-down (also called LL or predictive) and bottom-up (also called LR or shift-reduce). A top-down parser constructs a parse tree starting from the root and proceeding in a left-to-right depth-first traversal. A bottom-up parser constructs a parse tree starting from the leaves, again working left-to-right, and combining partial trees together when it recognizes the children of an internal node. The stack of a top-down parser contains a prediction of what will be seen in the future; the stack of a bottom-up parser contains a record of what has been seen in the past.
自上而下的解析器往往比较简单,无论是在解析有效字符串方面,还是在从无效字符串的错误中恢复方面。自下而上的解析器功能更强大,在某些情况下,它们更适合于更直观的结构化语法,但它们无法在右侧的任意位置嵌入动作例程(我们将在 C-4.5.1 节中更详细地讨论这一点)。这两种解析器都在实际编译器中使用,但自下而上的解析器更为常见。自上而下的解析器在代码和数据大小方面往往较小,但现代机器为这两种解析器都提供了充足的内存。
Top-down parsers tend to be simple, both in the parsing of valid strings and in the recovery from errors in invalid strings. Bottom-up parsers are more powerful, and in some cases lend themselves to more intuitively structured grammars, though they suffer from the inability to embed action routines at arbitrary points in a right-hand side (we discuss this point in more detail in Section C-4.5.1). Both varieties of parser are used in real compilers, though bottom-up parsers are more common. Top-down parsers tend to be smaller in terms of code and data size, but modern machines provide ample memory for either.
如果没有自动工具,可以手动构建扫描器和解析器。手工构建的扫描器非常简单,比较常见。手工构建的解析器通常仅限于自上而下的递归下降,最常用于相对简单的语言。自动生成扫描器和解析器具有可靠性更高、开发时间更短、易于修改和增强的优点。
Both scanners and parsers can be built by hand if an automatic tool is not available. Handbuilt scanners are simple enough to be relatively common. Hand-built parsers are generally limited to top-down recursive descent, and are most commonly used for comparatively simple languages. Automatic generation of the scanner and parser has the advantage of increased reliability, reduced development time, and easy modification and enhancement.
语言设计的各种特性会对语法分析的复杂性产生重大影响。在许多情况下,使编译器难以扫描或解析的特性也会使人类难以编写正确、可维护的代码。例子包括 Fortran 的词汇结构和Pascal 等语言的if…then…else语句。语言设计、实现和使用之间的这种相互作用将是本书其余部分反复出现的主题。
Various features of language design can have a major impact on the complexity of syntax analysis. In many cases, features that make it difficult for a compiler to scan or parse also make it difficult for a human being to write correct, maintainable code. Examples include the lexical structure of Fortran and the if… then … else statement of languages like Pascal. This interplay among language design, implementation, and use will be a recurring theme throughout the remainder of the book.
2.1 Write regular expressions to capture the following.
(a) C 中的字符串。这些字符串由双引号 (“) 分隔,并且不能包含换行符。当且仅当这些字符被前面的反斜杠“转义”时,它们才可以包含双引号或反斜杠字符。您可能会发现,引入简写符号来表示不属于指定小集合的任何字符会很有帮助。
(a) Strings in C. These are delimited by double quotes (“), and may not contain newline characters. They may contain double-quote or backslash characters if and only if those characters are “escaped” by a preceding backslash. You may find it helpful to introduce shorthand notation to represent any character that is not a member of a small specified set.
(b) Pascal 中的注释。这些注释由 (* 和 *) 或 { 和 } 分隔。不允许嵌套。
(b) Comments in Pascal. These are delimited by (* and *) or by { and }. They are not permitted to nest.
(c) C 中的数字常量。这些是八进制、十进制或十六进制整数,或十进制或十六进制浮点值。八进制整数以0开头,可能只包含数字0–7。十六进制整数以0x或0X开头,可能包含数字0–9和a/A–f/F。十进制浮点值具有小数部分(以点开头)或指数(以E或 e 开头)。与十进制整数不同,它可以以0开头。十六进制浮点值具有可选的小数部分和必需的指数(以P或p开头)。无论是十进制还是十六进制,都可以有数字在点的左侧、点的右侧或两者,指数本身以十进制表示,带有可选的前导+或−符号。整数可以以可选的U或u(表示“无符号”)和/或L或l(表示“长”)或LL或ll (表示“长长”)结尾。浮点值可以以可选的F或f(表示“浮点” - 单精度)或L或l(表示“长” - 双精度)结尾。
(c) Numeric constants in C. These are octal, decimal, or hexadecimal integers, or decimal or hexadecimal floating-point values. An octal integer begins with 0, and may contain only the digits 0–7. A hexadecimal integer begins with 0x or 0X, and may contain the digits 0–9 and a/A–f/F. A decimal floating-point value has a fractional portion (beginning with a dot) or an exponent (beginning with E or e). Unlike a decimal integer, it is allowed to start with 0. A hexadecimal floating-point value has an optional fractional portion and a mandatory exponent (beginning with P or p). In either decimal or hexadecimal, there may be digits to the left of the dot, the right of the dot, or both, and the exponent itself is given in decimal, with an optional leading + or − sign. An integer may end with an optional U or u (indicating “unsigned”), and/or L or l (indicating “long”) or LL or ll (indicating “long long”). A floating-point value may end with an optional F or f (indicating “float”—single precision) or L or l (indicating “long”—double precision).
(d) Ada 中的浮点常量。这些与示例 2.3中的实数定义相符,不同之处在于 (1) 小数点两边都需要一个数字,(2) 数字之间允许使用下划线,以及 (3) 可以通过用磅号将数字的非指数部分括起来并在前面加上十进制基数来指定替代数字基数(例如,16#6.a7#e+2)。在后一种情况下,字母a .. f(大写和小写)可以用作数字。在不适当的数字(例如十进制)中使用这些字母是一种错误,但扫描仪无需捕获。
(d) Floating-point constants in Ada. These match the definition of real in Example 2.3, except that (1) a digit is required on both sides of the decimal point, (2) an underscore is permitted between digits, and (3) an alternative numeric base may be specified by surrounding the nonexponent part of the number with pound signs, preceded by a base in decimal (e.g., 16#6.a7#e+2). In this latter case, the letters a .. f (both upper- and lowercase) are permitted as digits. Use of these letters in an inappropriate (e.g., decimal) number is an error, but need not be caught by the scanner.
(e) Scheme 中的不精确常数。Scheme 允许实数明确地不精确(不精确)。如果程序员想要使用相同数量的字符来表达所有常数,可以使用尖号 (#) 代替任何未知值的低位有效数字。无指数的十进制常数由一个或多个数字后跟零或更多尖号组成。可选的小数点可以放在开头、结尾或中间的任何地方。(需要说明的是,Scheme 中的数字实际上比这复杂得多。出于本练习的目的,请忽略您可能知道的有关符号、指数、基数、精确度和长度说明符以及复数或有理值的任何内容。)
(e) Inexact constants in Scheme. Scheme allows real numbers to be explicitly inexact (imprecise). A programmer who wants to express all constants using the same number of characters can use sharp signs (#) in place of any lower-significance digits whose values are not known. A base-10 constant without exponent consists of one or more digits followed by zero of more sharp signs. An optional decimal point can be placed at the beginning, the end, or anywhere in-between. (For the record, numbers in Scheme are actually a good bit more complicated than this. For the purposes of this exercise, please ignore anything you may know about sign, exponent, radix, exactness and length specifiers, and complex or rational values.)
(f) 美式财务数量。它们以美元符号 ($ 为前导)、可选的星号字符串(*——用于支票以防欺诈)、十进制数字字符串以及可选的小数部分(由一个小数点 (.) 和两个小数组成)。小数点左边的数字字符串可以由一个零 ( 0 ) 组成。否则,它不能以零开头。如果小数点左边的数字超过三位,则三组(从右边开始数)必须用逗号 (,) 分隔。例如:$**2,345.67。(可以随意使用“产品”来定义缩写,只要语言保持规则即可。)
(f) Financial quantities in American notation. These have a leading dollar sign ($), an optional string of asterisks (*—used on checks to discourage fraud), a string of decimal digits, and an optional fractional part consisting of a decimal point (.) and two decimal digits. The string of digits to the left of the decimal point may consist of a single zero (0). Otherwise it must not start with a zero. If there are more than three digits to the left of the decimal point, groups of three (counting from the right) must be separated by commas (,). Example: $**2,345.67. (Feel free to use “productions” to define abbreviations, so long as the language remains regular.)
2.2 以“圆圈和箭头”图的形式展示练习 2.1 的有限自动机。
2.2 Show (as “circles-and-arrows” diagrams) the finite automata for Exercise 2.1.
2.3 构建一个正则表达式,捕获除file、for和from之外的所有非空字母序列。为了方便记号,可以假设存在一个not运算符,该运算符以一组字母为参数,并匹配任何其他字母。评价为大型编程语言的关键字之外的所有字母序列构建正则表达式的实用性。
2.3 Build a regular expression that captures all nonempty sequences of letters other than file, for, and from. For notational convenience, you may assume the existence of a not operator that takes a set of letters as argument and matches any other letter. Comment on the practicality of constructing a regular expression for all sequences of letters other than the keywords of a large programming language.
2.4
2.4
(一个) 展示将图 2.7的构造应用于正则表达式字母(字母|数字)*所得的 NFA 。
(a) Show the NFA that results from applying the construction of Figure 2.7 to the regular expression letter ( letter | digit )*.
(b)应用 例 2.14所示的转换来创建等效 DFA。
(b) Apply the transformation illustrated by Example 2.14 to create an equivalent DFA.
(c)应用 例 2.15所示的变换来最小化 DFA。
(c) Apply the transformation illustrated by Example 2.15 to minimize the DFA.
2.5从 示例 2.3中的整数和小数的正则表达式开始,构造一个等效 NFA、子集集 DFA 和最小等效 DFA。确保将两种不同类型的 token 的最终状态分开(参见边栏 2.4)。如果您通过修改示例 2.13 至 2.15 中的机器来完成此练习,您可能会发现它更容易。
2.5 Starting with the regular expressions for integer and decimal in Example 2.3, construct an equivalent NFA, the set-of-subsets DFA, and the minimal equivalent DFA. Be sure to keep separate the final states for the two different kinds of token (see Sidebar 2.4). You may find the exercise easier if you undertake it by modifying the machines in Examples 2.13 through 2.15.
2.6 为计算器语言构建一个临时扫描器。作为输出,让它按顺序打印输入标记的列表。为简单起见,如果出现词汇错误,请随意停止。
2.6 Build an ad hoc scanner for the calculator language. As output, have it print a list, in order, of the input tokens. For simplicity, feel free to simply halt in the event of a lexical error.
2.7 用你最喜欢的脚本语言编写一个程序,从计算器语言程序中删除注释(示例 2.9)。
2.7 Write a program in your favorite scripting language to remove comments from programs in the calculator language (Example 2.9).
2.8 构建一个嵌套case语句的有限自动机,将输入的所有字母转换为小写,但 Pascal 风格的注释和字符串除外。Pascal 注释以 { 和 } 或 (* 和 *) 分隔。注释不能嵌套。Pascal 字符串以单引号 (' … ') 分隔。引号字符可以通过将其加倍来放置在字符串中 ( 'Madam, I' 'm Adam.' )。如果将用标准 Pascal (忽略大小写) 编写的程序输入到将大小写字母视为不同的编译器,则这种从大到小的映射非常有用。
2.8 Build a nested-case-statements finite automaton that converts all letters in its input to lower case, except within Pascal-style comments and strings. A Pascal comment is delimited by { and }, or by (* and *). Comments do not nest. A Pascal string is delimited by single quotes (' … '). A quote character can be placed in a string by doubling it ('Madam, I' 'm Adam.'). This upper-to-lower mapping can be useful if feeding a program written in standard Pascal (which ignores case) to a compiler that considers upper- and lowercase letters to be distinct.
2.9
2.9
(a)用英语描述正则表达式 a*(ba* ba*)*定义的语言。你的描述应该是高级特征——即使我们对同一种语言使用不同的正则表达式,这种描述仍然有意义。
(a) Describe in English the language defined by the regular expression a*(b a* b a*)*. Your description should be a high-level characterization—one that would still make sense if we were using a different regular expression for the same language.
(b) 编写一个能够生成相同语言的明确的上下文无关文法。
(b) Write an unambiguous context-free grammar that generates the same language.
(c) 使用部分(b)中的语法,对字符串baabaaabb进行规范(最右边)推导。
(c) Using your grammar from part (b), give a canonical (right-most) derivation of the string b a a b a a a b b.
2.10 给出一个捕获指数运算符右结合性的语法示例(例如 Fortran 中的 **)。
2.10 Give an example of a grammar that captures right associativity for an exponentiation operator (e.g., ** in Fortran).
2.11 证明下列文法是LL(1):
2.11 Prove that the following grammar is LL(1):
decl_tail →,decl
decl_tail →, decl
→ :身份证;
→ : ID ;
(最后一个ID是类型名称。)
(The final ID is meant to be a type name.)
2.12 考虑以下文法:G → S $$ S → AM M → S | ε A → a E | b AA E → a B | b A | ε B → b E | a BB
2.12 Consider the following grammar:
G → S $$
S → A M
M → S | ε
A → a E | b A A
E → a B | b A | ε
B → b E | a B B
(a) Describe in English the language that the grammar generates.
(b) 显示字符串abaa的解析树。
(b) Show a parse tree for the string a b a a.
(c) 该语法是 LL(1) 吗?如果是,则显示解析表;如果不是,则确定预测冲突。
(c) Is the grammar LL(1)? If so, show the parse table; if not, identify a prediction conflict.
2.13 考虑以下语法:stmt → assignment → subr_call assignment → id := expr subr_call → id ( arg_list ) expr → primary expr_tail expr_tail → op expr → ε primary → id → subr_call → ( expr ) op → + | − | * | / arg_list → expr args_tail args_tail →, arg_list → ε
2.13 Consider the following grammar:
stmt → assignment
→ subr_call
assignment → id := expr
subr_call → id ( arg_list)
expr → primary expr_tail
expr_tail → op expr
→ ε
primary → id
→ subr_call
→ ( expr )
op → + | − | * | /
arg_list → expr args_tail
args_tail →, arg_list
→ ε
(a) Construct a parse tree for the input string foo(a, b).
(b) 给出该相同字符串的规范(最右边)推导。
(b) Give a canonical (right-most) derivation of this same string.
(c) 证明该文法不是LL(1)文法。
(c) Prove that the grammar is not LL(1).
(d) 修改文法,使其成为LL(1)。
(d) Modify the grammar so that it isLL(1).
2.14 考虑由所有适当平衡的括号和方括号字符串组成的语言。
2.14 Consider the language consisting of all strings of properly balanced parentheses and brackets.
(a) Give LL(1) and SLR(1) grammars for this language.
(b) 给出相应的LL(1)和SLR(1)解析表。
(b) Give the corresponding LL(1) and SLR(1) parsing tables.
(c)对于每个语法,显示 ([]([]))[](())的解析树。
(c) For each grammar, show the parse tree for ([]([]))[](()).
(d) 跟踪分析器在构建这些树时的操作。
(d) Give a trace of the actions of the parsers in constructing these trees.
2.15 考虑以下上下文无关语法。G → GB → GN → ε B → ( E ) E → E ( E ) → ε N → ( L ] L → LE → L ( → ε
2.15 Consider the following context-free grammar.
G → G B
→ G N
→ ε
B → ( E )
E → E ( E )
→ ε
N → ( L ]
L → L E
→ L (
→ ε
(一个) 用英语描述该语法生成的语言。(提示:B代表“平衡”;N代表“不平衡”。)(您的描述应该是该语言的高级特征——与所选的特定语法无关。)
(a) Describe, in English, the language generated by this grammar. (Hint: B stands for “balanced”; N stands for “nonbalanced”.) (Your description should be a high-level characterization of the language—one that is independent of the particular grammar chosen.)
(b) 给出字符串 ((]() 的一棵分析树。
(b) Give a parse tree for the string ((]().
(c) 给出该相同字符串的规范(最右边)推导。
(c) Give a canonical (right-most) derivation of this same string.
(d) 我们的语法中的FIRST( E ) 是什么?FOLLOW( E ) 是什么?(回想一下,FIRST 和 FOLLOW 集合是为任意 CFG 中的符号定义的,与解析算法无关。)
(d) What is FIRST(E) in our grammar? What is FOLLOW(E)? (Recall that FIRST and FOLLOW sets are defined for symbols in an arbitrary CFG, regardless of parsing algorithm.)
(e) 鉴于其使用左递归,我们的语法显然不是 LL(1)。该语言有 LL(1) 语法吗?解释一下。
(e) Given its use of left recursion, our grammar is clearly not LL(1). Does this language have an LL(1) grammar? Explain.
2.16给出一个文法,该文法可以涵盖 C 语言中算术表达式的所有优先级,如图 6.1所示。(提示:这个练习有点乏味。你可能需要用文本编辑器而不是铅笔来完成它。)
2.16 Give a grammar that captures all levels of precedence for arithmetic expressions in C, as shown in Figure 6.1. (Hint: This exercise is somewhat tedious. You'll probably want to attack it with a text editor rather than a pencil.)
2.17扩展 图 2.25中的语法,使其包含if语句和while循环,具体方法如下:abs := n if n < 0 then abs := 0 - abs fi sum := 0 read count while count > 0 do read n sum := sum + n count := count - 1 od write sum语法应支持条件中的六种标准比较运算,以任意表达式作为操作数。它还应允许if或while语句主体中有任意数量的语句。
2.17 Extend the grammar of Figure 2.25 to include if statements and while loops, along the lines suggested by the following examples:
abs := n
if n < 0 then abs := 0 - abs fi
sum := 0
read count
while count > 0 do
read n
sum := sum + n
count := count - 1
od
write sum
Your grammar should support the six standard comparison operations in conditions, with arbitrary expressions as operands. It should also allow an arbitrary number of statements in the body of an if or while statement.
2.18 考虑以下简化 Lisp 子集的 LL(1) 语法:P → E $$ E → atom → ' E → ( E Es ) Es → E Es →
2.18 Consider the following LL(1) grammar for a simplified subset of Lisp:
P → E $$
E → atom
→ ’ E
→ ( E Es )
Es → E Es
→
(a) 什么是 FIRST( Es )?FOLLOW( E )?PREDICT( Es → ε )?
(a) What is FIRST(Es)? FOLLOW(E)? PREDICT(Es → ε)?
(b) 给出字符串(cdr '(abc)) $$ 的一棵分析树。
(b) Give a parse tree for the string (cdr '(a b c)) $$.
(c)显示 (cdr '(abc)) $$的最左导数。
(c) Show the left-most derivation of (cdr '(a b c)) $$.
(d)以 图 2.21的风格展示对同一输入进行表驱动自上而下解析的轨迹。
(d) Show a trace, in the style of Figure 2.21, of a table-driven top-down parse of this same input.
(e) 现在考虑在相同输入上运行的递归下降解析器。在匹配引号标记 (') 的位置,哪些递归下降例程将处于活动状态(即,哪些例程将在解析器的运行时堆栈上有一个框架)?
(e) Now consider a recursive descent parser running on the same input. At the point where the quote token (') is matched, which recursive descent routines will be active (i.e., what routines will have a frame on the parser's run-time stack)?
2.19 为由所有格式正确的正则表达式组成的语言编写自上而下和自下而上的文法。将所有运算符安排为左结合的。赋予 Kleene 闭包最高优先级,赋予交替最低优先级。
2.19 Write top-down and bottom-up grammars for the language consisting of all well-formed regular expressions. Arrange for all operators to be left-associative. Give Kleene closure the highest precedence and alternation the lowest precedence.
2.20 假设示例 2.8中的表达式语法与扫描器结合使用,该扫描器不会从输入中删除注释,而是将其作为标记返回。需要如何修改语法才能允许注释出现在输入中的任意位置?
2.20 Suppose that the expression grammar in Example 2.8 were to be used in conjunction with a scanner that did not remove comments from the input, but rather returned them as tokens. How would the grammar need to be modified to allow comments to appear at arbitrary places in the input?
2.21 为计算器语言构建一个完整的递归下降解析器。作为输出,让它打印匹配和预测的轨迹。
2.21 Build a complete recursive descent parser for the calculator language. As output, have it print a trace of its matches and predictions.
2.22 将你的解决方案扩展至练习 2.21,以构建一个显式的解析树。
2.22 Extend your solution to Exercise 2.21 to build an explicit parse tree.
2.23 将你的解决方案扩展至练习 2.21,以直接构建抽象语法树,而无需先构建解析树。
2.23 Extend your solution to Exercise 2.21 to build an abstract syntax tree directly, without constructing a parse tree first.
2.24 Pascal 的 悬空else问题并没有出现在其前身 Algol 60 中。为了避免对else匹配哪个then产生歧义,Algol 60 禁止在then子句内直接使用 if语句。Pascal 片段if C1 then if C2 then S1 else S2必须写成if C1 then begin if C2 then S1 end else S2或if C1 then begin if C2 then S1 else S2 end
在 Algol 60 中。说明如何编写强制执行此规则的条件语句语法。(提示:您需要在语法中区分条件语句和非条件语句;有些上下文会接受其中任何一种,有些只接受后者。)
2.24 The dangling else problem of Pascal was not shared by its predecessor Algol 60. To avoid ambiguity regarding which then is matched by an else, Algol 60 prohibited if statements immediately inside a then clause. The Pascal fragment
if C1 then if C2 then S1 else S2
had to be written as either
if C1 then begin if C2 then S1 end else S2
or
if C1 then begin if C2 then S1 else S2 end
in Algol 60. Show how to write a grammar for conditional statements that enforces this rule. (Hint: You will want to distinguish in your grammar between conditional statements and nonconditional statements; some contexts will accept either, some only the latter.)
2.25 充实算法的细节,以消除任意上下文无关文法中的左递归和公共前缀。
2.25 Flesh out the details of an algorithm to eliminate left recursion and common prefixes in an arbitrary context-free grammar.
2.26 在某些语言中,赋值可以出现在任何需要表达式的上下文中:表达式的值是赋值的右侧,作为副作用,它被放置在左侧。考虑这种语言的以下语法片段。解释为什么它不是 LL(1),并讨论可以做些什么来使其成为 LL(1)。expr → id := expr → term term_tail term_tail → + term term_tail | ε term → factor factor_tail factor_tail → * factor factor_tail | ε factor → ( expr ) | id
2.26 In some languages an assignment can appear in any context in which an expression is expected: the value of the expression is the right-hand side of the assignment, which is placed into the left-hand side as a side effect. Consider the following grammar fragment for such a language. Explain why it is not LL(1), and discuss what might be done to make it so.
expr → id := expr
→ term term_tail
term_tail → + term term_tail | ε
term → factor factor_tail
factor_tail → * factor factor_tail | ε
factor → ( expr ) | id
2.27为 示例 2.20中的id_list文法构建 CFSM ,并验证它可以自下而上地用零个前瞻标记进行解析。
2.27 Construct the CFSM for the id_list grammar in Example 2.20 and verify that it can be parsed bottom-up with zero tokens of look-ahead.
2.28修改 练习 2.27中的文法,使其允许id_list为空。该文法是否仍为 LR(0)?
2.28 Modify the grammar in Exercise 2.27 to allow an id_list to be empty. Is the grammar still LR(0)?
2.29 Repeat Example 2.36 using the grammar of Figure 2.15.
2.30 考虑以下声明列表的文法:decl_list → decl_list decl ; | decl ; decl → id : type type → int | real | char → array const .. const of type → record decl_list end构造该文法的 CFSM。使用它来追踪以下输入程序的解析(如图2.30所示): foo : record a : char; b : array 1 .. 2 of real; end;
2.30 Consider the following grammar for a declaration list:
decl_list → decl_list decl ; | decl ;
decl → id : type
type → int | real | char
→ array const .. const of type
→ record decl_list end
Construct the CFSM for this grammar. Use it to trace out a parse (as in Figure 2.30) for the following input program:
foo : record
a : char;
b : array 1 .. 2 of real;
end;
2.31–2.37 更深入。
2.31–2.37 In More Depth.
2.38 有些语言(例如 C)区分标识符中的大小写字母。其他语言(例如 Ada)则不区分。您更喜欢哪种惯例?为什么?
2.38 Some languages (e.g., C) distinguish between upper- and lowercase letters in identifiers. Others (e.g., Ada) do not. Which convention do you prefer? Why?
2.39 C 及其后代中的类型转换语法引入了潜在的歧义:(x)-y是减法,还是y的一元否定,转换为类型x?了解 C、C++、Java 和 C# 如何回答这个问题。讨论如何实现答案。
2.39 The syntax for type casts in C and its descendants introduces potential ambiguity: is (x)-y a subtraction, or the unary negation of y, cast to type x? Find out how C, C++, Java, and C# answer this question. Discuss how you would implement the answer(s).
2.40 您如何看待 Haskell、Occam 和 Python 使用缩进来界定控制结构(第 2.1.1 节)?您认为这种惯例会使程序构建和维护更容易还是更困难?为什么?
2.40 What do you think of Haskell, Occam, and Python's use of indentation to delimit control constructs (Section 2.1.1)? Would you expect this convention to make program construction and maintenance easier or harder? Why?
2.41 直接跳到14.4.2 节,了解脚本语言、编辑器、搜索工具等中使用的“正则表达式”。这些真的有规律吗?它们能表达什么,而2.1.1 节介绍的符号无法表达?
2.41 Skip ahead to Section 14.4.2 and learn about the “regular expressions” used in scripting languages, editors, search tools, and so on. Are these really regular? What can they express that cannot be expressed in the notation introduced in Section 2.1.1?
2.42 使用lex/flex重建练习 2.8的自动机。
2.42 Rebuild the automaton of Exercise 2.8 using lex/flex.
2.43查找 yacc/bison手册,或查阅编译器教科书 [ ALSU07,第 4.8.1 和 4.9.2 节] 以了解运算符优先级解析。解释如何使用它来简化练习 2.16的语法。
2.43 Find a manual for yacc/bison, or consult a compiler textbook [ALSU07, Secs. 4.8.1 and 4.9.2] to learn about operator precedence parsing. Explain how it could be used to simplify the grammar of Exercise 2.16.
2.44 使用lex/flex和yacc/bison为计算器语言构建一个解析器。让它输出其移位和归约的轨迹。
2.44 Use lex/flex and yacc/bison to construct a parser for the calculator language. Have it output a trace of its shifts and reductions.
2.45 使用 ANTLR 重复前面的练习。
2.45 Repeat the previous exercise using ANTLR.
2.46–2.47 更深入。
2.46–2.47 In More Depth.
本章对扫描和解析的介绍很简短。在有关解析理论 [ AU72 ] 和编译器构造 [ ALSU07、FCL10、App97、GBJ + 12、CT04 ] 的文本中可以找到更详细的内容。20 世纪 60 年代早期的许多编译器都使用了递归下降解析器。Lewis 和 Stearns [ LS68 ] 以及 Rosenkrantz 和 Stearns [ RS70 ] 发表了早期对 LL 语法和解析的正式研究。LR 解析的原始表述归功于 Knuth [ Knu65 ]。随着 DeRemer 发现 SLR 和 LALR 算法 [ DeR71 ] ,自下而上的解析变得实用。WL Johnson 等人 [ JPAR68 ] 描述了一种早期的扫描器生成器。Unix lex工具由 Lesk [ Les75 ] 提供。Yacc源自 SC Johnson [ Joh75 ]。
Our coverage of scanning and parsing in this chapter has of necessity been brief. Considerably more detail can be found in texts on parsing theory [AU72] and compiler construction [ALSU07, FCL10, App97, GBJ+12, CT04]. Many compilers of the early 1960s employed recursive descent parsers. Lewis and Stearns [LS68] and Rosenkrantz and Stearns [RS70] published early formal studies of LL grammars and parsing. The original formulation of LR parsing is due to Knuth [Knu65]. Bottom-up parsing became practical with DeRemer's discovery of the SLR and LALR algorithms [DeR71]. W. L. Johnson et al. [JPAR68] describe an early scanner generator. The Unix lex tool is due to Lesk [Les75]. Yacc is due to S. C. Johnson [Joh75].
关于形式语言理论的更多细节可以在各种教科书中找到,包括 Hopcroft、Motwani 和 Ullman 的教科书 [ HMU07 ] 和Sipser [ Sip13 ]。Kleene [ Kle56 ] 以及 Rabin 和 Scott [ RS59 ] 证明了正则表达式与有限自动机的等价性。15有限自动机无法识别嵌套结构的证明基于Bar-Hillel、Perles 和 Shamir [ BHPS61 ] 提出的泵引理。Chomsky [ Cho56 ] 首次在自然语言环境中探索了上下文无关语法。Backus 和 Naur 独立开发了 BNF,用于 Algol 60 [ NBB + 63 ] 的句法描述。Ginsburg 和 Rice [ GR62 ] 认识到这两种符号的等价性。Chomsky [ Cho62 ] 和 Evey [ Eve63 ] 证明了上下文无关语法和下推自动机的等价性。
Further details on formal language theory can be found in a variety of textbooks, including those of Hopcroft, Motwani, and Ullman [HMU07] and Sipser [Sip13]. Kleene [Kle56] and Rabin and Scott [RS59] proved the equivalence of regular expressions and finite automata.15 The proof that finite automata are unable to recognize nested constructs is based on a theorem known as the pumping lemma, due to Bar-Hillel, Perles, and Shamir [BHPS61]. Context-free grammars were first explored by Chomsky [Cho56] in the context of natural language. Independently, Backus and Naur developed BNF for the syntactic description of Algol 60 [NBB+63]. Ginsburg and Rice [GR62] recognized the equivalence of the two notations. Chomsky [Cho62] and Evey [Eve63] demonstrated the equivalence of context-free grammars and push-down automata.
Fischer 等人的文本 [ FCL10 ] 包含对错误恢复和修复技术的出色概述,并引用了其他工作。第 C-2.3.5 节中描述的递归下降解析器的短语级恢复机制由 Wirth [ Wir76 ,第 5.9 节]提出。第 C-2.3.5 节中描述的表驱动 LL 解析器的局部最小成本恢复机制由 Fischer、Milton 和 Quiring [ FMQ80 ]提出。Dion 于 1978 年发表了局部最小成本自下而上的修复算法 [ Dio78 ]。该算法非常复杂,需要非常大的预计算表。McKenzie、Yeatman 和 De Vere 随后展示了如何在没有预计算表的情况下实现相同的修复,虽然时间成本较高,但仍在可接受的范围内 [ MYD95 ]。
Fischer et al.'s text [FCL10] contains an excellent survey of error recovery and repair techniques, with references to other work. The phrase-level recovery mechanism for recursive descent parsers described in Section C-2.3.5 is due to Wirth [Wir76, Sec. 5.9]. The locally least-cost recovery mechanism for table-driven LL parsers described in Section C-2.3.5 is due to Fischer, Milton, and Quiring [FMQ80]. Dion published a locally least-cost bottom-up repair algorithm in 1978 [Dio78]. It is quite complex, and requires very large precomputed tables. McKenzie, Yeatman, and De Vere subsequently showed how to effect the same repairs without the precomputed tables, at a higher but still acceptable cost in time [MYD95].
早期的语言(例如 Fortran、Algol 和 Lisp)之所以被称为“高级”语言,是因为它们的语法和语义比它们要取代的汇编语言抽象得多(距离硬件更远)。抽象使得编写可在各种机器上运行良好的程序成为可能,也使程序更容易被人类理解。虽然机器独立性仍然很重要,但编程的简易性仍然是推动现代语言设计的主动力。本章是解决语言设计核心问题的六章中的第一章。(其他三章是第 6 章至第 10章。)当前的讨论大部分将围绕名称的概念展开。
Early languages such as Fortran, Algol, and Lisp were termed “high level” because their syntax and semantics were significantly more abstract—farther from the hardware—than those of the assembly languages they were intended to supplant. Abstraction made it possible to write programs that would run well on a wide variety of machines. It also made programs significantly easier for human beings to understand. While machine independence remains important, it is primarily ease of programming that continues to drive the design of modern languages. This chapter is the first of six to address core issues in language design. (The others are Chapters 6 through 10.) Much of the current discussion will revolve around the notion of names.
名称是用于表示其他内容的助记字符串。大多数语言中的名称都是标识符(字母数字标记),但某些其他符号(例如 + 或 :=)也可以是名称。名称允许我们使用符号标识符而不是地址等低级概念来引用变量、常量、操作、类型等。名称在“抽象”一词的第二个含义中也是必不可少的。在这个第二个含义中,抽象是程序员将名称与可能复杂的程序片段相关联的过程,然后可以从其目的或功能的角度来思考,而不是从如何实现该功能的角度来思考。通过隐藏不相关的细节,抽象降低了概念复杂性,使程序员可以在任何特定时间专注于程序文本的可管理子集。子程序是控制抽象:它们允许程序员将任意复杂的代码隐藏在简单的接口后面。类是数据抽象:它们允许程序员将数据表示细节隐藏在(相对)简单的操作集后面。
A name is a mnemonic character string used to represent something else. Names in most languages are identifiers (alphanumeric tokens), though certain other symbols, such as + or :=, can also be names. Names allow us to refer to variables, constants, operations, types, and so on using symbolic identifiers rather than low-level concepts like addresses. Names are also essential in the context of a second meaning of the word abstraction. In this second meaning, abstraction is a process by which the programmer associates a name with a potentially complicated program fragment, which can then be thought of in terms of its purpose or function, rather than in terms of how that function is achieved. By hiding irrelevant details, abstraction reduces conceptual complexity, making it possible for the programmer to focus on a manageable subset of the program text at any particular time. Subroutines are control abstractions: they allow the programmer to hide arbitrarily complicated code behind a simple interface. Classes are data abstractions: they allow the programmer to hide data representation details behind a (comparatively) simple set of operations.
我们将讨论与名称相关的几个主要问题。第 3.1 节介绍了绑定时间的概念,它不仅指将名称绑定到它所代表的事物,而且通常指解决语言实现中的任何设计决策的概念。第 3.2 节概述了用于为对象分配和释放存储空间的各种机制,并区分了对象的生存期和名称与该对象的绑定的生存期。1大多数名称到对象的绑定仅在给定高级程序的有限区域内可用。第 3.3 节探讨了定义此区域的范围规则;第 3.4 节(主要在配套网站上)考虑了它们的实现。
We will look at several major issues related to names. Section 3.1 introduces the notion of binding time, which refers not only to the binding of a name to the thing it represents, but also in general to the notion of resolving any design decision in a language implementation. Section 3.2 outlines the various mechanisms used to allocate and deallocate storage space for objects, and distinguishes between the lifetime of an object and the lifetime of a binding of a name to that object.1 Most name-to-object bindings are usable only within a limited region of a given high-level program. Section 3.3 explores the scope rules that define this region; Section 3.4 (mostly on the companion site) considers their implementation.
在程序中给定点生效的完整绑定集称为当前引用环境。3.5节讨论了别名,其中多个名称可能引用给定范围内的给定对象,以及重载,其中一个名称可能引用给定范围内的多个对象,具体取决于引用的上下文。3.6节通过考虑引用环境如何绑定到作为参数传递、从函数返回或存储在变量中的子例程,扩展了范围规则的概念。3.7节讨论了宏扩展,它可以通过文本替换引入新名称,有时与语言的其余部分不一致。最后,3.8 节(主要在配套站点上)讨论了单独编译。
The complete set of bindings in effect at a given point in a program is known as the current referencing environment. Section 3.5 discusses aliasing, in which more than one name may refer to a given object in a given scope, and overloading, in which a name may refer to more than one object in a given scope, depending on the context of the reference. Section 3.6 expands on the notion of scope rules by considering the ways in which a referencing environment may be bound to a subroutine that is passed as a parameter, returned from a function, or stored in a variable. Section 3.7 discusses macro expansion, which can introduce new names via textual substitution, sometimes in ways that are at odds with the rest of the language. Finally, Section 3.8 (mostly on the companion site) discusses separate compilation.
绑定是两个事物之间的关联,例如名称和它所命名的事物。绑定时间是创建绑定的时间,或者更一般地说,是做出任何实现决策的时间(我们可以将其视为将答案绑定到问题上)。决策可能在很多不同的时间进行绑定:
A binding is an association between two things, such as a name and the thing it names. Binding time is the time at which a binding is created or, more generally, the time at which any implementation decision is made (we can think of this as binding an answer to a question). There are many different times at which decisions maybe bound:
语言设计时:在大多数语言中,控制流构造、基本(原始)类型集、可用于创建复杂类型的构造函数以及语言语义的许多其他方面都是在设计语言时选择的。
Language design time: In most languages, the control-flow constructs, the set of fundamental (primitive) types, the available constructors for creating complex types, and many other aspects of language semantics are chosen when the language is designed.
语言实现时间:大多数语言手册将各种问题留给语言实现者自行决定。典型(但绝不是普遍)示例包括基本类型的精度(位数)、I/O 与操作系统文件概念的耦合,以及堆栈和堆的组织和最大大小。
Language implementation time: Most language manuals leave a variety of issues to the discretion of the language implementor. Typical (though by no means universal) examples include the precision (number of bits) of the fundamental types, the coupling of I/O to the operating system's notion of files, and the organization and maximum sizes of the stack and heap.
编写程序时间:程序员当然会选择算法、数据结构和名称。
Program writing time: Programmers, of course, choose algorithms, data structures, and names.
编译时:编译器选择高级结构到机器代码的映射,包括内存中静态定义的数据的布局。
Compile time: Compilers choose the mapping of high-level constructs to machine code, including the layout of statically defined data in memory.
链接时间:由于大多数编译器支持单独编译(在不同时间编译程序的不同模块),并且依赖于标准子例程库的可用性,因此程序通常在各个模块通过链接器连接在一起之前是不完整的。链接器会选择模块相对于彼此的整体布局,并解析模块间引用。当一个模块中的名称引用另一个模块中的对象时,两者之间的绑定直到链接时才会最终确定。
Link time: Since most compilers support separate compilation—compiling different modules of a program at different times—and depend on the availability of a library of standard subroutines, a program is usually not complete until the various modules are joined together by a linker. The linker chooses the overall layout of the modules with respect to one another, and resolves intermodule references. When a name in one module refers to an object in another module, the binding between the two is not finalized until link time.
加载时间:加载时间是指操作系统将程序加载到内存中以便运行的时间点。在原始操作系统中,程序中对象的机器地址选择直到加载时才最终确定。大多数现代操作系统区分虚拟地址和物理地址。虚拟地址是在链接时选择的;物理地址实际上可以在运行时更改。处理器的内存管理硬件在运行时的每个单独指令期间将虚拟地址转换为物理地址。
Load time: Load time refers to the point at which the operating system loads the program into memory so that it can run. In primitive operating systems, the choice of machine addresses for objects within the program was not finalized until load time. Most modern operating systems distinguish between virtual and physical addresses. Virtual addresses are chosen at link time; physical addresses can actually change at run time. The processor's memory management hardware translates virtual addresses into physical addresses during each individual instruction at run time.
运行时:运行时实际上是一个非常宽泛的术语,涵盖了从执行开始到结束的整个过程。值与变量的绑定发生在运行时,其他许多因语言而异的决策也是如此。运行时包括程序启动时间、模块进入时间、阐述时间(声明首次“被看到”的时间点)、子例程调用时间、块进入时间以及表达式求值时间/语句执行。
Run time: Run time is actually a very broad term that covers the entire span from the beginning to the end of execution. Bindings of values to variables occur at run time, as do a host of other decisions that vary from language to language. Run time subsumes program start-up time, module entry time, elaboration time (the point at which a declaration is first “seen”), subroutine call time, block entry time, and expression evaluation time/statement execution.
静态和动态这两个术语通常分别用于指代在运行前和运行时绑定的事物。显然,“静态”是一个粗略的术语。“动态”也是如此。
The terms static and dynamic are generally used to refer to things bound before run time and at run time, respectively. Clearly “static” is a coarse term. So is “dynamic.”
基于编译器的语言实现往往比基于解释器的实现更高效,因为它们会更早做出决定。例如,编译器会在程序运行之前分析一次全局变量声明的语法和语义。它会决定这些变量在内存中的布局,并生成高效的代码来访问它们,无论它们出现在程序中的何处。相比之下,纯解释器必须在每次程序开始执行时分析声明。在最坏的情况下,解释器可能会在每次调用子例程时重新分析子例程中的本地声明。如果调用出现在深度嵌套的循环中,那么仅分析一次声明的编译器所实现的节省可能非常大。正如我们将在在下一节中,编译器通常无法在编译时预测局部变量的地址,因为变量的空间将在堆栈上动态分配,但它可以安排变量出现在运行时某个寄存器指向的位置的固定偏移量处。
Compiler-based language implementations tend to be more efficient than interpreter-based implementations because they make earlier decisions. For example, a compiler analyzes the syntax and semantics of global variable declarations once, before the program ever runs. It decides on a layout for those variables in memory and generates efficient code to access them wherever they appear in the program. A pure interpreter, by contrast, must analyze the declarations every time the program begins execution. In the worst case, an interpreter may reanalyze the local declarations within a subroutine each time that subroutine is called. If a call appears in a deeply nested loop, the savings achieved by a compiler that is able to analyze the declarations only once may be very large. As we shall see in the following section, a compiler will not usually be able to predict the address of a local variable at compile time, since space for the variable will be allocated dynamically on a stack, but it can arrange for the variable to appear at a fixed offset from the location pointed to by a certain register at run time.
某些语言难以编译,因为它们的语义要求将基本决策推迟到运行时,这通常是为了增加语言的灵活性或表现力。例如,大多数脚本语言将所有类型检查推迟到运行时。对任意类型(类)对象的引用可以分配给任意命名的变量,只要程序永远不会对未准备好处理的对象应用运算符(调用其方法)即可。这种多态性形式- 适用于多种类型的对象或表达式 - 允许程序员编写异常灵活和通用的代码。我们将在以后的几个部分中再次提到多态性,包括7.1.2、7.3、10.1.1和14.4.4。
Some languages are difficult to compile because their semantics require fundamental decisions to be postponed until run time, generally in order to increase the flexibility or expressiveness of the language. Most scripting languages, for example, delay all type checking until run time. References to objects of arbitrary types (classes) can be assigned into arbitrary named variables, as long as the program never ends up applying an operator to (invoking a method of) an object that is not prepared to handle it. This form of polymorphism—applicability to objects or expressions of multiple types—allows the programmer to write unusually flexible and general-purpose code. We will mention polymorphism again in several future sections, including 7.1.2, 7.3, 10.1.1, and 14.4.4.
在任何有关名称和绑定的讨论中,区分名称和它们所引用的对象并识别几个关键事件非常重要:
In any discussion of names and bindings, it is important to distinguish between names and the objects to which they refer, and to identify several key events:
■ Creation and destruction of objects
■ 绑定的创建和销毁
■ Creation and destruction of bindings
■ 停用和重新激活可能暂时无法使用的绑定
■ Deactivation and reactivation of bindings that may be temporarily unusable
■ 对变量、子例程、类型等的引用,所有这些都使用绑定
■ References to variables, subroutines, types, and so on, all of which use bindings
名称到对象绑定的创建和销毁之间的时间段称为绑定的生命周期。类似地,对象的创建和销毁之间的时间是对象的生命周期。这些生命周期不一定需要一致。特别是,即使给定的名称不再可用于访问对象,对象仍可以保留其值和被访问的潜力。例如,当变量通过引用传递给子例程时(通常在 Fortran 中或 C++ 中使用“ & ”参数),参数名称和传递的变量之间的绑定的生命周期短于变量本身的生命周期。名称到对象绑定的生命周期也可能长于对象的生命周期,尽管这通常是程序错误的标志。例如,如果通过 C++ new运算符创建的对象作为&参数传递,然后在子例程返回之前释放(delete),则会发生这种情况。对不再存在的对象的绑定称为悬垂引用。悬垂引用将在第 3.6 节和8.5.2节中进一步讨论。
The period of time between the creation and the destruction of a name-to-object binding is called the binding's lifetime. Similarly, the time between the creation and destruction of an object is the object's lifetime. These lifetimes need not necessarily coincide. In particular, an object may retain its value and the potential to be accessed even when a given name can no longer be used to access it. When a variable is passed to a subroutine by reference, for example (as it typically is in Fortran or with '&' parameters in C++), the binding between the parameter name and the variable that was passed has a lifetime shorter than that of the variable itself. It is also possible, though generally a sign of a program bug, for a name-to-object binding to have a lifetime longer than that of the object. This can happen, for example, if an object created via the C++ new operator is passed as a & parameter and then deallocated (delete-ed) before the subroutine returns. A binding to an object that is no longer live is called a dangling reference. Dangling references will be discussed further in Sections 3.6 and 8.5.2.
对象生命周期通常对应于三种主要存储分配机制之一,用于管理对象的空间:
Object lifetimes generally correspond to one of three principal storage allocation mechanisms, used to manage the object's space:
1. 静态对象被赋予一个绝对地址,该地址在整个程序执行过程中都保留下来。
1. Static objects are given an absolute address that is retained throughout the program's execution.
2. 堆栈对象按照后进先出的顺序进行分配和释放,通常与子程序调用和返回结合使用。
2. Stack objects are allocated and deallocated in last-in, first-out order, usually in conjunction with subroutine calls and returns.
3. 堆对象可能在任意时间被分配和释放。它们需要更通用(且昂贵)的存储管理算法。
3. Heap objects maybe allocated and deallocated at arbitrary times. They require a more general (and expensive) storage management algorithm.
全局变量是静态对象的明显示例,但不是唯一的示例。构成程序机器码的指令也可以被视为静态分配的对象。我们将在3.3.1 节中看到一些示例,这些示例是单个子例程的本地变量,但它们的值在一次调用到下一次调用时保持不变;它们的空间是静态分配的。数字和字符串值常量文字也是静态分配的,例如A = B/14.7或printf(“hello, world\n”)之类的语句。(小常量通常存储在指令本身内;较大的常量则分配到单独的位置。)最后,大多数编译器会生成各种表,供运行时支持例程用于调试、动态类型检查、垃圾收集、异常处理和其他目的;这些也是静态分配的。静态分配的对象(例如,指令、常量和某些运行时表)的值在程序执行期间不应改变,通常分配在受保护的只读内存中,因此任何无意的写入尝试都会导致处理器中断,从而允许操作系统宣布运行时错误。
Global variables are the obvious example of static objects, but not the only one. The instructions that constitute a program's machine code can also be thought of as statically allocated objects. We shall see examples in Section 3.3.1 of variables that are local to a single subroutine, but retain their values from one invocation to the next; their space is statically allocated. Numeric and string-valued constant literals are also statically allocated, for statements such as A = B/14.7 or printf(“hello, world\n”). (Small constants are often stored within the instruction itself; larger ones are assigned a separate location.) Finally, most compilers produce a variety of tables that are used by run-time support routines for debugging, dynamic type checking, garbage collection, exception handling, and other purposes; these are also statically allocated. Statically allocated objects whose value should not change during program execution (e.g., instructions, constants, and certain run-time tables) are often allocated in protected, read-only memory, so that any inadvertent attempt to write to them will cause a processor interrupt, allowing the operating system to announce a run-time error.
在许多语言中,命名常量必须具有可在编译时确定的值。通常,指定常量值的表达式只允许包含其他已知常量、内置函数和算术运算符。这种命名常量与常量文字有时称为清单常量或编译时常量。清单常量始终可以静态分配,即使它们对于递归子例程来说是本地的:多个实例可以共享同一位置。
In many languages a named constant is required to have a value that can be determined at compile time. Usually the expression that specifies the constant's value is permitted to include only other known constants and built-in functions and arithmetic operators. Named constants of this sort, together with constant literals, are sometimes called manifest constants or compile-time constants. Manifest constants can always be allocated statically, even if they are local to a recursive subroutine: multiple instances can share the same location.
在其他语言(例如 C 和 Ada)中,常量只是在阐述(初始化)时间之后无法更改的变量。它们的值虽然不变,但有时可能取决于在运行时才知道的其他值。当此类阐述时间常量位于递归子例程的本地时,必须在堆栈上分配。C# 分别使用const和readonly关键字来区分编译时常量和阐述时间常量。
In other languages (e.g., C and Ada), constants are simply variables that cannot be changed after elaboration (initialization) time. Their values, though unchanging, can sometimes depend on other values that are not known until run time. Such elaboration-time constants, when local to a recursive subroutine, must be allocated on the stack. C# distinguishes between compile-time and elaboration-time constants using the const and readonly keywords, respectively.
堆栈的维护是子程序调用序列(调用者在调用前后立即执行的代码)以及子程序本身的序言(在开头执行的代码)和尾声(在结尾执行的代码)的责任。有时术语“调用序列”用于指调用者、序言和尾声的组合操作。我们将在第 9.2 节中更详细地研究调用序列。
Maintenance of the stack is the responsibility of the subroutine calling sequence—the code executed by the caller immediately before and after the call—and of the prologue (code executed at the beginning) and epilogue (code executed at the end) of the subroutine itself. Sometimes the term “calling sequence” is used to refer to the combined operations of the caller, the prologue, and the epilogue. We will study calling sequences in more detail in Section 9.2.
虽然无法在编译时预测堆栈帧的位置(编译器通常无法判断堆栈中可能已经有哪些其他帧),但帧内对象的偏移量通常可以静态确定。此外,编译器可以安排(在调用序列或序言中)特定寄存器(称为帧指针)始终指向当前子例程帧内的已知位置。需要访问当前帧内的局部变量或调用帧顶部附近的参数的代码可以通过将预定的偏移量添加到帧指针中的值来实现。正如我们在 C-5.3.1 节中讨论的那样,几乎每个处理器都提供了一种位移寻址机制,允许将此添加隐式指定为普通加载或存储指令的一部分。在大多数语言实现中,堆栈向较低地址“向下”增长。一些机器提供特殊的推送和弹出指令来假设这种增长方向。局部变量、临时变量和簿记信息通常与帧指针具有负偏移量。参数和返回通常具有正偏移量;它们驻留在呼叫者的框架中。
While the location of a stack frame cannot be predicted at compile time (the compiler cannot in general tell what other frames may already be on the stack), the offsets of objects within a frame usually can be statically determined. Moreover, the compiler can arrange (in the calling sequence or prologue) for a particular register, known as the frame pointer to always point to a known location within the frame of the current subroutine. Code that needs to access a local variable within the current frame, or an argument near the top of the calling frame, can do so by adding a predetermined offset to the value in the frame pointer. As we discuss in Section C-5.3.1, almost every processor provides a displacement addressing mechanism that allows this addition to be specified implicitly as part of an ordinary load or store instruction. The stack grows “downward” toward lower addresses in most language implementations. Some machines provide special push and pop instructions that assume this direction of growth. Local variables, temporaries, and bookkeeping information typically have negative offsets from the frame pointer. Arguments and returns typically have positive offsets; they reside in the caller's frame.
即使在没有递归的语言中,使用堆栈来存储局部变量也比静态分配局部变量更有优势。在大多数程序中,子程序之间的潜在调用模式不允许所有这些子程序同时处于活动状态。因此,当前活动子程序的局部变量所需的总空间很少与所有子程序(无论是否活动)的总空间一样大。因此,堆栈在运行时所需的内存可能比静态分配所需的内存少得多。
Even in a language without recursion, it can be advantageous to use a stack for local variables, rather than allocating them statically. In most programs the pattern of potential calls among subroutines does not permit all of those subroutines to be active at the same time. As a result, the total space needed for local variables of currently active subroutines is seldom as large as the total space across all subroutines, active or not. A stack may therefore require substantially less memory at run time than would be required for static allocation.
堆是存储区域,可以在任意时间分配和释放子块。2动态分配的链接数据结构片段以及完全通用字符串、列表和集合等对象都需要堆,这些对象的大小可能会因赋值语句或其他更新操作而发生变化。
A heap is a region of storage in which subblocks can be allocated and deallocated at arbitrary times.2 Heaps are required for the dynamically allocated pieces of linked data structures, and for objects such as fully general character strings, lists, and sets, whose size may change as a result of an assignment statement or other update operation.
许多存储管理算法维护一个链表,即空闲列表,其中包含当前未使用的堆块。最初,该列表由一个包含整个堆的块组成。每次分配请求时,算法都会在列表中搜索合适大小的块。使用首次适合算法,我们选择列表中第一个足够大以满足请求的块。使用最佳适合算法,我们搜索整个列表以找到足够大的最小块以满足请求。无论哪种情况,如果所选块明显大于所需块,我们都会将其分成两部分,并将不需要的部分作为较小的块返回到空闲列表。(如果不需要的部分大小低于某个最小阈值,我们可能会将其作为内部碎片留在已分配的块中。)当一个块被释放并返回到空闲列表时,我们会检查物理相邻的块中是否有一个或两个是空闲的;如果是,我们会将它们合并起来。
Many storage-management algorithms maintain a single linked list—the free list—of heap blocks not currently in use. Initially the list consists of a single block comprising the entire heap. At each allocation request the algorithm searches the list for a block of appropriate size. With a first fit algorithm we select the first block on the list that is large enough to satisfy the request. With a best fit algorithm we search the entire list to find the smallest block that is large enough to satisfy the request. In either case, if the chosen block is significantly larger than required, then we divide it into two and return the unneeded portion to the free list as a smaller block. (If the unneeded portion is below some minimum threshold in size, we may leave it in the allocated block as internal fragmentation.) When a block is deallocated and returned to the free list, we check to see whether either or both of the physically adjacent blocks are free; if so, we coalesce them.
直观地看,人们会认为最佳匹配算法能够更好地为大型请求保留大型块。同时,它的分配成本比首次匹配算法更高,因为它必须始终搜索整个列表,并且往往会导致大量非常小的“剩余”块。哪种方法(首次匹配或最佳匹配)可降低外部碎片率取决于大小请求的分布。
Intuitively, one would expect a best fit algorithm to do a better job of reserving large blocks for large requests. At the same time, it has higher allocation cost than a first fit algorithm, because it must always search the entire list, and it tends to result in a larger number of very small “left-over” blocks. Which approach—first fit or best fit—results in lower external fragmentation depends on the distribution of size requests.
在任何维护单个空闲列表的算法中,分配成本与空闲块的数量成线性关系。为了将此成本降低为常数,某些存储管理算法为不同大小的块维护单独的空闲列表。每个请求都四舍五入到下一个标准大小(以内部碎片为代价)并从适当的列表中分配。实际上,堆被分成“池”,每个标准大小一个。划分可以是静态的,也可以是动态的。两种常见的动态池调整机制称为伙伴系统和斐波那契堆。在伙伴系统中,标准块大小是 2 的幂。如果需要一个大小为 2k 的块,但没有可用的块,则将大小为 2k+1 的块一分为二。其中一半用于满足请求;另一半放在第k个空闲列表中。当一个块被释放时,如果其“伙伴”(创建它的分割的另一半)空闲,它会与该伙伴合并。斐波那契堆与之类似,但标准大小使用斐波那契数,而不是 2 的幂。该算法稍微复杂一些,但内部碎片化程度略低,因为斐波那契数列的增长速度比 2k慢。
In any algorithm that maintains a single free list, the cost of allocation is linear in the number of free blocks. To reduce this cost to a constant, some storage management algorithms maintain separate free lists for blocks of different sizes. Each request is rounded up to the next standard size (at the cost of internal fragmentation) and allocated from the appropriate list. In effect, the heap is divided into “pools,” one for each standard size. The division may be static or dynamic. Two common mechanisms for dynamic pool adjustment are known as the buddy system and the Fibonacci heap. In the buddy system, the standard block sizes are powers of two. If a block of size 2k is needed, but none is available, a block of size 2k+1 is split in two. One of the halves is used to satisfy the request; the other is placed on the kth free list. When a block is deallocated, it is coalesced with its “buddy”—the other half of the split that created it—if that buddy is free. Fibonacci heaps are similar, but use Fibonacci numbers for the standard sizes, instead of powers of two. The algorithm is slightly more complex, but leads to slightly lower internal fragmentation, because the Fibonacci sequence grows more slowly than 2k.
外部碎片的问题在于,堆满足请求的能力可能会随着时间的推移而下降。多个空闲列表可能会有所帮助,因为它们将小块聚集在相对接近的物理位置,但它们并不能消除问题。即使所需的总空间小于堆的大小,也总是有可能设计出无法满足的请求序列。如果内存在大小池之间静态划分,则只需超过给定大小的最大请求数即可。如果池是动态重新调整的,则可以按物理地址的顺序分配大量小块,然后释放每个其他小块,从而“棋盘化”堆,留下小块空闲和已分配的交替模式。为了消除外部碎片,我们必须准备好通过移动已分配的块来压缩堆。这项任务很复杂,因为需要查找和更新对正在移动的块的所有未完成引用。我们将在第8.5.3 节中进一步讨论压缩。
The problem with external fragmentation is that the ability of the heap to satisfy requests may degrade over time. Multiple free lists may help, by clustering small blocks in relatively close physical proximity, but they do not eliminate the problem. It is always possible to devise a sequence of requests that cannot be satisfied, even though the total space required is less than the size of the heap. If memory is partitioned among size pools statically, one need only exceed the maximum number of requests of a given size. If pools are dynamically readjusted, one can “checkerboard” the heap by allocating a large number of small blocks and then deallocating every other one, in order of physical address, leaving an alternating pattern of small free and allocated blocks. To eliminate external fragmentation, we must be prepared to compact the heap, by moving already-allocated blocks. This task is complicated by the need to find and update all outstanding references to a block that is being moved. We will discuss compaction further in Section 8.5.3.
基于堆的对象分配总是由程序中的某些特定操作触发:实例化一个对象、附加到列表的末尾、将一个长值赋给之前的短字符串等等。在某些语言(例如 C、C++ 和 Rust)中,释放也是显式的。然而,正如我们将在第8.5 节中看到的那样,许多语言指定当无法再从任何程序变量访问对象时,应隐式释放对象。这种语言的运行时库必须提供垃圾收集机制来识别和回收无法访问的对象。大多数函数式和脚本语言都需要垃圾收集,许多较新的命令式语言(包括 Java 和 C#)也是如此。
Allocation of heap-based objects is always triggered by some specific operation in a program: instantiating an object, appending to the end of a list, assigning a long value into a previously short string, and so on. Deallocation is also explicit in some languages (e.g., C, C++, and Rust). As we shall see in Section 8.5, however, many languages specify that objects are to be deallocated implicitly when it is no longer possible to reach them from any program variable. The run-time library for such a language must then provide a garbage collection mechanism to identify and reclaim unreachable objects. Most functional and scripting languages require garbage collection, as do many more recent imperative languages, including Java and C#.
支持显式释放的传统论点是实现简单性和执行速度。即使是简单的自动垃圾收集实现也会给具有丰富类型系统的语言的实现增加相当大的复杂性,即使是最复杂的垃圾收集器也会在某些程序中消耗大量时间。如果程序员能够正确识别对象生命周期的结束,而无需进行太多的运行时记账,那么执行速度可能会更快。
The traditional arguments in favor of explicit deallocation are implementation simplicity and execution speed. Even naive implementations of automatic garbage collection add significant complexity to the implementation of a language with a rich type system, and even the most sophisticated garbage collector can consume nontrivial amounts of time in certain programs. If the programmer can correctly identify the end of an object's lifetime, without too much run-time bookkeeping, the result is likely to be faster execution.
但是,支持自动垃圾收集的理由是令人信服的:手动释放错误是实际程序中最常见且代价最高的错误之一。如果过早释放某个对象,程序可能会遵循悬垂引用,访问现在由另一个对象使用的内存。如果对象在其生命周期结束时未释放,则程序可能会“泄漏内存”,最终耗尽堆空间。释放错误非常难以识别和修复。随着时间的推移,许多语言设计者和程序员开始将自动垃圾收集视为语言的一项基本特性。垃圾收集算法已经得到改进,从而降低了运行时开销;语言实现总体上变得更加复杂,从而降低了自动收集的边际复杂性;并且尖端应用程序变得更大、更复杂,使得自动收集的好处更引人注目。
The argument in favor of automatic garbage collection, however, is compelling: manual deallocation errors are among the most common and costly bugs in real-world programs. If an object is deallocated too soon, the program may follow a dangling reference, accessing memory now used by another object. If an object is not deallocated at the end of its lifetime, then the program may “leak memory,” eventually running out of heap space. Deallocation errors are notoriously difficult to identify and fix. Over time, many language designers and programmers have come to consider automatic garbage collection an essential language feature. Garbage-collection algorithms have improved, reducing their run-time overhead; language implementations have become more complex in general, reducing the marginal complexity of automatic collection; and leading-edge applications have become larger and more complex, making the benefits of automatic collection ever more compelling.
绑定处于活动状态的程序文本区域即其作用域。在大多数现代语言中,绑定的作用域是静态确定的,即在编译时确定。例如,在 C 语言中,我们在进入子例程时引入新的作用域。我们为本地对象创建绑定,并停用被同名本地对象隐藏(不可见)的全局对象的绑定。在退出子例程时,我们销毁本地变量的绑定并重新激活任何隐藏的全局对象的绑定。这些绑定操作乍一看似乎是运行时操作,但它们不需要执行任何代码:绑定处于活动状态的程序部分完全在编译时确定。我们可以查看 C 程序,并根据纯文本规则知道哪些名称在程序的哪个位置引用哪些对象。因此,C 被称为静态作用域(有些作者说是词法作用域3)。其他语言(包括 APL、Snobol、Tcl 和 Lisp 的早期方言)都是动态作用域的:它们的绑定取决于运行时的执行流程。我们将在第 3.3.1 节和第 3.3.6节中更详细地介绍静态和动态作用域。
The textual region of the program in which a binding is active is its scope. In most modern languages, the scope of a binding is determined statically, that is, at compile time. In C, for example, we introduce a new scope upon entry to a subroutine. We create bindings for local objects and deactivate bindings for global objects that are hidden (made invisible) by local objects of the same name. On subroutine exit, we destroy bindings for local variables and reactivate bindings for any global objects that were hidden. These manipulations of bindings may at first glance appear to be run-time operations, but they do not require the execution of any code: the portions of the program in which a binding is active are completely determined at compile time. We can look at a C program and know which names refer to which objects at which points in the program based on purely textual rules. For this reason, C is said to be statically scoped (some authors say lexically scoped3). Other languages, including APL, Snobol, Tcl, and early dialects of Lisp, are dynamically scoped: their bindings depend on the flow of execution at run time. We will examine static and dynamic scoping in more detail in Sections 3.3.1 and 3.3.6.
除了谈论“绑定的范围”之外,我们有时还会将“范围”一词单独用作名词,而不考虑特定的绑定。非正式地说,范围是一个最大大小的程序区域,其中没有绑定发生变化(或至少没有绑定被破坏 - 更多信息请参见第 3.3.3 节)。通常,范围是模块、类、子例程或结构化控制流语句的主体,有时称为块。在 C 系列语言中,它将用 {…} 括号分隔。
In addition to talking about the “scope of a binding,” we sometimes use the word “scope” as a noun all by itself, without a specific binding in mind. Informally, a scope is a program region of maximal size in which no bindings change (or at least none are destroyed—more on this in Section 3.3.3). Typically, a scope is the body of a module, class, subroutine, or structured control-flow statement, sometimes called a block. In C family languages it would be delimited with {…} braces.
Algol 68 和 Ada 使用术语“精化”来指代控制首次进入范围时声明变为活动的过程。精化需要创建绑定。在许多语言中,它还需要为本地对象分配堆栈空间,并可能分配初始值。在 Ada 中,它可能涉及许多其他事情,包括执行错误检查或堆空间分配代码、传播异常以及创建并发执行的任务(将在第 13 章中讨论)。
Algol 68 and Ada use the term elaboration to refer to the process by which declarations become active when control first enters a scope. Elaboration entails the creation of bindings. In many languages, it also entails the allocation of stack space for local objects, and possibly the assignment of initial values. In Ada it can entail a host of other things, including the execution of error-checking or heap-space-allocating code, the propagation of exceptions, and the creation of concurrently executing tasks (to be discussed in Chapter 13).
在程序执行的任何给定点,活动绑定的集合称为当前引用环境。该集合主要由静态或动态范围规则决定。我们将看到,引用环境通常对应于一系列范围,可以按顺序检查这些范围以找到给定名称的当前绑定。
At any given point in a program's execution, the set of active bindings is called the current referencing environment. The set is principally determined by static or dynamic scope rules. We shall see that a referencing environment generally corresponds to a sequence of scopes that can be examined (in order) to find the current binding for a given name.
在某些情况下,引用环境还取决于(术语使用令人困惑的)绑定规则。具体而言,当将对子程序S的引用存储在变量中、作为参数传递给另一个子程序或作为函数值返回时,需要确定何时选择S的引用环境- 即何时在对S 的引用和S的引用环境之间进行绑定。两个主要选项是深度绑定(在首次创建引用时进行选择)和浅绑定(在最终使用引用时进行选择)。我们将在第 3.6 节中更详细地研究这些选项。
In some cases, referencing environments also depend on what are (in a confusing use of terminology) called binding rules. Specifically, when a reference to a subroutine S is stored in a variable, passed as a parameter to another subroutine, or returned as a function value, one needs to determine when the referencing environment for S is chosen—that is, when the binding between the reference to S and the referencing environment of S is made. The two principal options are deep binding, in which the choice is made when the reference is first created, and shallow binding, in which the choice is made when the reference is finally used. We will examine these options in more detail in Section 3.6.
在具有静态(词法)作用域的语言中,名称和对象之间的绑定可以在编译时通过检查程序文本来确定,而无需考虑运行时的控制流。通常,给定名称的“当前”绑定位于匹配声明中,该声明的块最接近程序中的给定点,尽管我们将看到,这个基本主题有很多变体。
In a language with static (lexical) scoping, the bindings between names and objects can be determined at compile time by examining the text of the program, without consideration of the flow of control at run time. Typically, the “current” binding for a given name is found in the matching declaration whose block most closely surrounds a given point in the program, though as we shall see there are many variants on this basic theme.
最简单的静态作用域规则可能是 Basic 的早期版本,其中只有一个全局作用域。事实上,只有几百个可能的名称,每个名称都由一个字母和一个数字组成。没有显式声明;变量通过使用而隐式声明。
The simplest static scope rule is probably that of early versions of Basic, in which there was only a single, global scope. In fact, there were only a few hundred possible names, each of which consisted of a letter optionally followed by a digit. There were no explicit declarations; variables were declared implicitly by virtue of being used.
在 Fortran 90 之前的版本中,范围规则稍微复杂一些,但也没有那么复杂。Fortran 区分全局变量和局部变量。局部变量的范围仅限于它出现的子程序;它在其他地方不可见。变量声明是可选的。如果未声明变量,则假定它是当前子程序的局部变量,并且如果其名称以字母 I-N 开头,则为整型,否则为实型。(程序员可以指定不同的隐式声明约定。在 Fortran 90 及其后续版本中,程序员还可以关闭隐式声明,这样使用未声明的变量就会成为编译时错误。)
Scope rules are somewhat more complex in (pre-Fortran 90) Fortran, though not much more. Fortran distinguishes between global and local variables. The scope of a local variable is limited to the subroutine in which it appears; it is not visible elsewhere. Variable declarations are optional. If a variable is not declared, it is assumed to be local to the current subroutine and to be of type integer if its name begins with the letters I–N, or real otherwise. (Different conventions for implicit declarations can be specified by the programmer. In Fortran 90 and its successors, the programmer can also turn off implicit declarations, so that use of an undeclared variable becomes a compile-time error.)
从语义上讲,局部 Fortran 变量(对象本身和名称到对象的绑定)的生存期包含变量子例程的一次执行。程序员可以使用显式 save 语句来覆盖此规则。(许多其他语言中也存在类似的机制:在 C 中,将变量声明为static;在 Algol 中,将变量声明为own。)保存的(static、own)变量的生存期包含程序的整个执行。编译器不会为每次调用子例程创建一个逻辑上独立的对象,而是创建一个对象,该对象在从一次子例程调用到下一次子例程调用之间保留其值。(当然,当子例程未执行时,名称到变量的绑定处于非活动状态,因为名称超出了范围。)
Semantically, the lifetime of a local Fortran variable (both the object itself and the name-to-object binding) encompasses a single execution of the variable's subroutine. Programmers can override this rule by using an explicit save statement. (Similar mechanisms appear in many other languages: in C one declares the variable static; in Algol one declares it own.) A save-ed (static, own) variable has a lifetime that encompasses the entire execution of the program. Instead of a logically separate object for every invocation of the subroutine, the compiler creates a single object that retains its value from one invocation of the subroutine to the next. (The name-to-variable binding, of course, is inactive when the subroutine is not executing, because the name is out of scope.)
Algol 60 中引入了子程序相互嵌套的功能,这是许多后续语言的功能,包括 Ada、ML、Common Lisp、Python、Scheme、Swift 和(在有限范围内)Fortran 90。其他语言(包括 C 及其后代)允许类或其他范围嵌套。正如 Fortran 子程序的局部变量对其他子程序不可见一样,在 Algol 系列语言中,在范围内声明的任何常量、类型、变量或子程序在该范围之外都是不可见的。更正式地说,Algol 风格的嵌套产生了从名称到对象的绑定的最近嵌套范围规则:在声明中引入的名称在声明它的范围中是已知的,并且在每个内部嵌套的范围中都是已知的,除非它被一个或多个嵌套范围中的另一个同名声明隐藏。要找到与名称的给定用法相对应的对象,我们会在当前最内层范围内查找具有该名称的声明。如果有,则它定义了名称的活动绑定。否则,我们在紧邻的范围内寻找声明。我们继续向外,依次检查周围的范围,直到到达程序的外层嵌套级别,其中声明了全局对象。如果在任何级别都找不到声明,则程序有错误。
The ability to nest subroutines inside each other, introduced in Algol 60, is a feature of many subsequent languages, including Ada, ML, Common Lisp, Python, Scheme, Swift, and (to a limited extent) Fortran 90. Other languages, including C and its descendants, allow classes or other scopes to nest. Just as the local variables of a Fortran subroutine are not visible to other subroutines, any constants, types, variables, or subroutines declared within a scope are not visible outside that scope in Algol-family languages. More formally, Algol-style nesting gives rise to the closest nested scope rule for bindings from names to objects: a name that is introduced in a declaration is known in the scope in which it is declared, and in each internally nested scope, unless it is hidden by another declaration of the same name in one or more nested scopes. To find the object corresponding to a given use of a name, we look for a declaration with that name in the current, innermost scope. If there is one, it defines the active binding for the name. Otherwise, we look for a declaration in the immediately surrounding scope. We continue outward, examining successively surrounding scopes, until we reach the outer nesting level of the program, where global objects are declared. If no declaration is found at any level, then the program is in error.
许多语言都提供了内置或预定义对象的集合,如 I/O 例程、数学函数,在某些情况下还包括整数和字符等类型。通常认为这些对象是在额外的、不可见的最外层作用域中声明的,该作用域围绕着声明全局对象的作用域。上一段中描述的绑定搜索终止于这个额外的、最外层作用域(如果存在),而不是声明全局对象的作用域。这种最外层作用域约定使程序员可以定义一个全局对象,其名称与某个预定义对象相同(其“声明”因此被隐藏,使其不可见)。
Many languages provide a collection of built-in, or predefined objects, such as I/O routines, mathematical functions, and in some cases types such as integer and char. It is common to consider these to be declared in an extra, invisible, outermost scope, which surrounds the scope in which global objects are declared. The search for bindings described in the previous paragraph terminates at this extra, outermost scope, if it exists, rather than at the scope in which global objects are declared. This outermost scope convention makes it possible for a programmer to define a global object whose name is the same as that of some predefined object (whose “declaration” is thereby hidden, making it invisible).
名称到对象的绑定如果被同名的嵌套声明所隐藏,则称其作用域中有漏洞。在某些语言中,名称被隐藏的对象在嵌套作用域中根本无法访问(除非它有多个名称)。在其他语言中,程序员可以通过应用限定符或作用域解析运算符来访问名称的外部含义。例如,在 Ada 中,名称可以以其声明作用域的名称作为前缀,使用的语法类似于记录中字段的规范。例如, My_proc.X引用子例程My_proc中X的声明,而不管是否在词汇上更接近的作用域中声明了其他X。在不允许子例程嵌套的 C++ 中, :: X引用X的全局声明,而不管当前子例程是否也具有X。5
A name-to-object binding that is hidden by a nested declaration of the same name is said to have a hole in its scope. In some languages, the object whose name is hidden is simply inaccessible in the nested scope (unless it has more than one name). In others, the programmer can access the outer meaning of a name by applying a qualifier or scope resolution operator. In Ada, for example, a name may be prefixed by the name of the scope in which it is declared, using syntax that resembles the specification of fields in a record. My_proc.X, for example, refers to the declaration of X in subroutine My_proc, regardless of whether some other X has been declared in a lexically closer scope. In C++, which does not allow subroutines to nest, ::X refers to a global declaration of X, regardless of whether the current subroutine also has an X.5
我们已经看到(第 3.2.2 节),编译器可以安排一个帧指针寄存器在运行时指向当前正在执行的子程序的帧。使用此寄存器作为位移(寄存器加偏移量)寻址的基数,目标代码可以访问当前子程序内的对象。但是词汇上围绕子程序中的对象呢?为了找到这些,我们需要一种方法来在运行时找到与这些作用域相对应的帧。由于嵌套子程序可能会调用外部作用域中的例程,因此运行时堆栈帧的顺序不一定与词汇嵌套的顺序相对应。尽管如此,我们可以肯定堆栈中已经存在周围作用域的某个帧,因为除非当前子程序可见,否则不可能调用它,而除非周围作用域处于活动状态,否则它不可能可见。 (在某些语言中,实际上可以保存对嵌套子程序的引用,然后在周围范围不再活跃时调用它。我们将这种可能性推迟到第 3.6.2 节。)
We have already seen (Section 3.2.2) that the compiler can arrange for a frame pointer register to point to the frame of the currently executing subroutine at run time. Using this register as a base for displacement (register plus offset) addressing, target code can access objects within the current subroutine. But what about objects in lexically surrounding subroutines? To find these we need a way to find the frames corresponding to those scopes at run time. Since a nested subroutine may call a routine in an outer scope, the order of stack frames at run time may not necessarily correspond to the order of lexical nesting. Nonetheless, we can be sure that there is some frame for the surrounding scope already in the stack, since the current subroutine could not have been called unless it was visible, and it could not have been visible unless the surrounding scope was active. (It is actually possible in some languages to save a reference to a nested subroutine, and then call it when the surrounding scope is no longer active. We defer this possibility to Section 3.6.2.)
到目前为止,我们在讨论中忽略了一个重要的细节:假设在块B中的某个地方声明了一个对象x 。 x的作用域是否包括声明之前的B部分?如果是,那么x是否可以在该部分代码中使用?换句话说,表达式E可以引用当前作用域中声明的任何名称,还是只能引用在作用域中在 E 之前声明的名称?
In our discussion so far we have glossed over an important subtlety: suppose an object x is declared somewhere within block B. Does the scope of x include the portion of B before the declaration, and if so can x actually be used in that portion of the code? Put another way, can an expression E refer to any name declared in the current scope, or only to names that are declared before E in the scope?
包括 Algol 60 和 Lisp 在内的几种早期语言都要求所有声明都出现在其范围的开头。人们一开始可能会认为这条规则可以避免上一段中的问题,但事实并非如此,因为声明可以相互引用。6
Several early languages, including Algol 60 and Lisp, required that all declarations appear at the beginning of their scope. One might at first think that this rule would avoid the questions in the preceding paragraph, but it does not, because declarations may refer to one another.6
在许多语言中,包括 Algol 60、C89 和 Ada,局部变量不仅可以在任何子程序的开头声明,还可以在任何begin…end({…})块的顶部声明。其他语言,包括 Algol 68、C 和 C 的所有后代,甚至更加灵活,允许在语句出现的任何位置进行声明。在大多数语言中,嵌套声明会隐藏任何具有相同名称的外部声明(如果外部声明是当前子程序的本地声明,则 Java 和 C# 会将其视为静态语义错误)。
In many languages, including Algol 60, C89, and Ada, local variables can be declared not only at the beginning of any subroutine, but also at the top of any begin… end ({…}) block. Other languages, including Algol 68, C, and all of C's descendants, are even more flexible, allowing declarations wherever a statement may appear. In most languages a nested declaration hides any outer declaration with the same name (Java and C# make it a static semantic error if the outer declaration is local to the current subroutine).
无需运行时工作来为嵌套块中声明的变量分配或释放空间;它们的空间可以包含在子程序序言中分配并在结尾中释放的局部变量的总空间中。练习 3.9考虑如何最小化所需的总空间。
No run-time work is needed to allocate or deallocate space for variables declared in nested blocks; their space can be included in the total space for local variables allocated in the subroutine prologue and deallocated in the epilogue. Exercise 3.9 considers how to minimize the total space required.
构建任何大型软件时,一个重要挑战是将工作分摊给程序员,以便工作能够同时在多个方面进行。这种模块化工作主要依赖于信息隐藏的概念,即尽可能使对象和算法对系统中不需要它们的部分不可见。适当模块化的代码通过最小化理解任何给定部分所需的信息量来减少程序员的“认知负荷”。系统。在设计良好的程序中,模块之间的接口尽可能“窄”(即简单),任何可能改变的设计决策都隐藏在单个模块内。
An important challenge in the construction of any large body of software is to divide the effort among programmers in such a way that work can proceed on multiple fronts simultaneously. This modularization of effort depends critically on the notion of information hiding, which makes objects and algorithms invisible, whenever possible, to portions of the system that do not need them. Properly modularized code reduces the “cognitive load” on the programmer by minimizing the amount of information required to understand any given portion of the system. In a well-designed program the interfaces among modules are as “narrow” (i.e., simple) as possible, and any design decision that is likely to change is hidden inside a single module.
信息隐藏对于软件维护(错误修复和增强)至关重要,这往往远远超过大多数商业软件的初始开发成本。除了减少认知负荷外,隐藏还可以降低名称冲突的风险:可见名称越少,新引入的名称与已使用名称相同的可能性就越小。它还保护了数据抽象的完整性:任何试图访问其所属模块之外的对象的行为都会导致编译器发出“未定义符号”错误消息。最后,它有助于区分运行时错误:如果变量具有意外值,我们通常可以确定修改它的代码在变量的范围内。
Information hiding is crucial for software maintenance (bug fixes and enhancement), which tends to significantly outweigh the cost of initial development for most commercial software. In addition to reducing cognitive load, hiding reduces the risk of name conflicts: with fewer visible names, there is less chance that a newly introduced name will be the same as one already in use. It also safeguards the integrity of data abstractions: any attempt to access an object outside of the module to which it belongs will cause the compiler to issue an “undefined symbol” error message. Finally, it helps to compartmentalize run-time errors: if a variable takes on an unexpected value, we can generally be sure that the code that modified it is in the variable's scope.
不幸的是,嵌套子程序提供的信息隐藏仅限于其生命周期与隐藏它们的子程序的生命周期相同的对象。当控制从子程序返回时,其局部变量将不再有效:它们的值将被丢弃。我们已经看到了这个问题的部分解决方案,即 Fortran 中的 save 语句以及 C 和 Algol 中的静态和自身变量。
Unfortunately, the information hiding provided by nested subroutines is limited to objects whose lifetime is the same as that of the subroutine in which they are hidden. When control returns from a subroutine, its local variables will no longer be live: their values will be discarded. We have seen a partial solution to this problem in the form of the save statement in Fortran and the static and own variables of C and Algol.
模块允许将一组对象(子程序、变量、类型等)封装在一起,使得 (1) 模块内部的对象彼此可见,但 (2) 模块内部的对象在外部可能不可见,除非它们被导出,以及 (3) 模块外部的对象在内部可能不可见,除非它们被导入。 导入和导出约定在不同语言之间差别很大,但在所有情况下,只有对象的可见性会受到影响;模块不会影响其所包含对象的生命周期。
A module allows a collection of objects—subroutines, variables, types, and so on—to be encapsulated in such a way that (1) objects inside are visible to each other, but (2) objects on the inside may not be visible on the outside unless they are exported, and (3) objects on the outside may not be visible on the inside unless they are imported. Import and export conventions vary significantly from one language to another, but in all cases, only the visibility of objects is affected; modules do not affect the lifetime of the objects they contain.
模块是 20 世纪 70 年代末和 80 年代初的主要语言创新之一;它们出现在 Clu 7(称之为集群)、Modula(1、2 和 3)、Turing 和 Ada 83 等中。以更现代的形式,它们也出现在 Haskell、C++、Java、C# 和所有主要脚本语言中。包括 Ada、Java 和 Perl 在内的几种语言使用术语包而不是模块。其他语言,包括 C++、C# 和 PHP,使用命名空间。可以通过使用 C 的单独编译功能在一定程度上模拟模块;我们将在C-3.8 节中讨论这种可能性。
Modules were one of the principal language innovations of the late 1970s and early 1980s; they appeared in Clu7 (which called them clusters), Modula (1, 2, and 3), Turing, and Ada 83, among others. In more modern form, they also appear in Haskell, C++, Java, C#, and all the major scripting languages. Several languages, including Ada, Java, and Perl, use the term package instead of module. Others, including C++, C#, and PHP, use namespace. Modules can be emulated to some degree through use of the separate compilation facilities of C; we discuss this possibility in Section C-3.8.
作为模块使用的一个例子,请考虑图 3.6中所示的伪随机数生成器。如侧边栏 3.5 中所述,此模块(命名空间)通常放在其自己的文件中,然后在 C++ 程序中需要它的地方导入。
As an example of the use of modules, consider the pseudorandom number generator shown in Figure 3.6. As discussed in Sidebar 3.5, this module (namespace) would typically be placed in its own file, and then imported wherever it is needed in a C++ program.
有些语言允许程序员指定从模块导出的名称只能以受限的方式使用。例如,变量可以以只读方式导出,或者类型可以以不透明方式导出,这意味着可以声明该类型的变量,将其作为参数传递给模块的子例程,并可能进行比较或赋值,但不能以任何其他方式操作。
Some languages allow the programmer to specify that names exported from modules be usable only in restricted ways. Variables may be exported read-only, for example, or types may be exported opaquely, meaning that variables of that type may be declared, passed as arguments to the module's subroutines, and possibly compared or assigned to one another, but not manipulated in any other way.
必须明确导入名称的模块被称为封闭作用域。 扩展而言,不需要导入的模块被称为开放作用域。 导入用于记录程序:它们要求模块指定它依赖于程序其余部分的方式,从而提高模块化程度。 它们还通过避免导入任何不需要的内容来减少名称冲突。 Modula(1、2 和 3)和 Haskell 中的模块是封闭的。 C++ 代表了一种越来越常见的选项,其中名称会自动导出,但只有在使用模块名称限定时才可在外部使用- 除非它们被另一个作用域明确“导入”(例如,使用 C++ using 指令),此时它们可以无限定使用。 这个选项,我们可以称之为选择性开放模块,也出现在 Ada、Java、C# 和 Python 等中。
Modules into which names must be explicitly imported are said to be closed scopes. By extension, modules that do not require imports are said to be open scopes. Imports serve to document the program: they increase modularity by requiring a module to specify the ways in which it depends on the rest of the program. They also reduce name conflicts by refraining from importing anything that isn't needed. Modules are closed in Modula (1, 2, and 3) and Haskell. C++ is representative of an increasingly common option, in which names are automatically exported, but are available on the outside only when qualified with the module name—unless they are explicitly “imported” by another scope (e.g., with the C++ using directive), at which point they are available unqualified. This option, which we might call selectively open modules, also appears in Ada, Java, C#, and Python, among others.
模块类型和类之间的区别在于,后者拥有一对强大的功能,而前者却没有——即继承和动态方法分派。8继承允许将新类定义为现有类的扩展或细化。动态方法分派允许细化的类覆盖其父类中操作的定义,并允许在运行时根据特定对象是属于子类还是仅属于父类来选择定义。
The difference between module types and classes is a powerful pair of features found together in the latter but not the former—namely, inheritance and dynamic method dispatch.8 Inheritance allows new classes to be defined as extensions or refinements of existing classes. Dynamic method dispatch allows a refined class to override the definition of an operation in its parent class, and for the choice among definitions to be made at run time, on the basis of whether a particular object belongs to the child class or merely to the parent.
继承促进了一种编程风格,在这种风格中,所有或大多数操作都被视为属于对象,而新对象可以从现有对象继承许多操作,而无需重写代码。类起源于 Simula-67,并在 Smalltalk 中得到进一步发展。它们出现在许多现代语言中,包括 Eiffel、OCaml、C++、Java、C# 和几种脚本语言,尤其是 Python 和 Ruby。继承机制也可以在某些通常不被认为是面向对象的语言中找到,包括 Modula-3、Ada 95 和 Oberon。我们将在第10 章和第 14.4.4 节中研究继承、动态分派及其对作用域规则的影响。
Inheritance facilitates a programming style in which all or most operations are thought of as belonging to objects, and in which new objects can inherit many of their operations from existing objects, without the need to rewrite code. Classes have their roots in Simula-67, and were further developed in Smalltalk. They appear in many modern languages, including Eiffel, OCaml, C++, Java, C#, and several scripting languages, notably Python and Ruby. Inheritance mechanisms can also be found in certain languages that are not usually considered object-oriented, including Modula-3, Ada 95, and Oberon. We will examine inheritance, dynamic dispatch, and their impact on scope rules in Chapter 10 and in Section 14.4.4.
模块类型和类(忽略与继承相关的问题)只需要对第 3.3.4 节中为模块定义的范围规则进行简单的更改。模块类型或类的每个实例A (例如每个rand_gen)都有该模块或类变量的单独副本。执行A的某个操作时,这些变量就可见了。如果将A作为参数传递给某个其他实例B的操作,那么这些变量也可能间接地对其他实例 B 的操作可见。此规则使得在大多数面向对象语言中可以构造二进制(或多元)操作,以操作一个类的多个实例的变量(字段)。
Module types and classes (ignoring issues related to inheritance) require only simple changes to the scope rules defined for modules in Section 3.3.4. Every instance A of a module type or class (e.g., every rand_gen) has a separate copy of the module or class's variables. These variables are then visible when executing one of A's operations. They may also be indirectly visible to the operations of some other instance B if A is passed as a parameter to one of those operations. This rule makes it possible in most object-oriented languages to construct binary (or more-ary) operations that can manipulate the variables (fields) of more than one instance of a class.
在具有动态作用域的语言中,名称和对象之间的绑定取决于运行时的控制流,特别是子例程的调用顺序。与上一节讨论的静态作用域规则相比,动态作用域规则通常非常简单:给定名称的“当前”绑定是执行期间最近遇到的绑定,并且尚未通过从其作用域返回而被破坏。
In a language with dynamic scoping, the bindings between names and objects depend on the flow of control at run time, and in particular on the order in which subroutines are called. In comparison to the static scope rules discussed in the previous section, dynamic scope rules are generally quite simple: the “current” binding for a given name is the one encountered most recently during execution, and not yet destroyed by returning from its scope.
具有动态作用域的语言包括 APL、Snobol、Tcl、T E X(本书所使用的排版语言)以及 Lisp 的早期方言[ MAE + 65、Moo78、TM81 ] 和 Perl。9因为控制流通常无法提前预测,所以具有动态作用域的语言中名称和对象之间的绑定通常无法由编译器确定。因此,具有动态作用域的语言中的许多语义规则成为动态语义而不是静态语义的问题。例如,表达式中的类型检查和子程序调用中的参数检查通常必须推迟到运行时。为了进行所有这些检查,具有动态作用域的语言倾向于被解释而不是被编译。
Languages with dynamic scoping include APL, Snobol, Tcl, TEX (the typesetting language with which this book was created), and early dialects of Lisp [MAE+65, Moo78, TM81] and Perl.9 Because the flow of control cannot in general be predicted in advance, the bindings between names and objects in a language with dynamic scoping cannot in general be determined by a compiler. As a result, many semantic rules in a language with dynamic scoping become a matter of dynamic semantics rather than static semantics. Type checking in expressions and argument checking in subroutine calls, for example, must in general be deferred until run time. To accommodate all these checks, languages with dynamic scoping tend to be interpreted, rather than compiled.
为了跟踪静态作用域程序中的名称,编译器依赖于称为符号表的数据抽象。本质上,符号表是一本字典:它将名称映射到编译器所知道的信息。最基本的操作是插入新的映射(名称到对象的绑定)或查找给定名称的已有信息。静态作用域规则允许给定的名称对应于程序不同部分的不同对象(从而对应于不同的信息),这增加了复杂性。大多数静态作用域的变体都可以通过用enter_scope和leave_scope操作扩充基本字典式符号表来处理,以跟踪可见性。表中的任何内容都不会被删除;整个结构在整个编译过程中都保留,然后保存以供调试器或运行时反射(类型查找)机制使用。
To keep track of the names in a statically scoped program, a compiler relies on a data abstraction called a symbol table. In essence, the symbol table is a dictionary: it maps names to the information the compiler knows about them. The most basic operations are to insert a new mapping (a name-to-object binding) or to look up the information that is already present for a given name. Static scope rules add complexity by allowing a given name to correspond to different objects—and thus to different information—in different parts of the program. Most variations on static scoping can be handled by augmenting a basic dictionary-style symbol table with enter_scope and leave_scope operations to keep track of visibility. Nothing is ever deleted from the table; the entire structure is retained throughout compilation, and then saved for use by debuggers or run-time reflection (type lookup) mechanisms.
在具有动态作用域的语言中,解释器(或编译器的输出)必须在运行时执行类似于符号表插入和查找的操作。原则上,编译器中用于符号表的任何组织都可用于跟踪解释器中的名称到对象的绑定,反之亦然。实际上,动态作用域的实现倾向于采用两种特定组织之一:关联列表或中央引用表。
In a language with dynamic scoping, an interpreter (or the output of a compiler) must perform operations analogous to symbol table insert and lookup at run time. In principle, any organization used for a symbol table in a compiler could be used to track name-to-object bindings in an interpreter, and vice versa. In practice, implementations of dynamic scoping tend to adopt one of two specific organizations: an association list or a central reference table.
更深入地
IN MORE DEPTH
具有可视性支持的符号表可以用几种不同的方式实现。配套站点上介绍了一种由 LeBlanc 和 Cook [ CL83 ] 提出的有吸引力的方法,以及关联列表和中央引用表。
A symbol table with visibility support can be implemented in several different ways. One appealing approach, due to LeBlanc and Cook [CL83], is described on the companion site, along with both association lists and central reference tables.
关联列表(简称A 列表)只是名称/值对的列表。当用于实现动态作用域时,它的功能相当于一个堆栈:遇到新声明时将其推送,并在其出现的作用域末尾弹出。通过从顶部向下搜索列表来找到绑定。中央引用表通过维护从名称到其当前含义的显式映射来避免线性时间搜索的需要。查找速度更快,但作用域的进入和退出稍微复杂一些,并且保存引用环境以供将来使用变得更加困难(我们将在第3.6.1 节中进一步讨论这个问题)。
An association list (or A-list for short) is simply a list of name/value pairs. When used to implement dynamic scoping it functions as a stack: new declarations are pushed as they are encountered, and popped at the end of the scope in which they appeared. Bindings are found by searching down the list from the top. A central reference table avoids the need for linear-time search by maintaining an explicit mapping from names to their current meanings. Lookup is faster, but scope entry and exit are somewhat more complex, and it becomes substantially more difficult to save a referencing environment for future use (we discuss this issue further in Section 3.6.1).
到目前为止,在我们对命名和范围的讨论中,我们假设在程序中任何给定点,名称和可见对象之间存在一对一映射。事实并非如此。在程序中的同一点引用同一对象的两个或多个名称被称为别名。在程序中的给定点可以引用多个对象的名称被称为重载。重载又与更一般的多态性主题相关,多态性允许子例程或其他程序片段根据其参数的类型以不同的方式运行。
So far in our discussion of naming and scopes we have assumed that there is a one-to-one mapping between names and visible objects at any given point in a program. This need not be the case. Two or more names that refer to the same object at the same point in the program are said to be aliases. A name that can refer to more than one object at a given point in the program is said to be overloaded. Overloading is in turn related to the more general subject of polymorphism, which allows a subroutine or other program fragment to behave in different ways depending on the types of its arguments.
别名的简单示例出现在许多编程语言的变体记录和联合中(我们将在C-8.1.3 节中详细讨论这些功能)。
Simple examples of aliases occur in the variant records and unions of many programming languages (we will discuss these features detail in Section C-8.1.3).
类型类可以基于自身构建。例如,Haskell 0rd类包含所有支持运算符<、>、<=和>=的Eq类型。Num 类(略微简化)包含所有支持加法、减法和乘法的Eq类型。除了使重载比大多数语言更明确之外,类型类还可以指定某些多态函数只能在其参数属于支持某些特定重载函数的类型时使用(有关此主题的更多信息,请参阅边栏 7.7)。
Type classes can build upon themselves. The Haskell 0rd class, for example, encompasses all Eq types that also support the operators <, >, <=, and >=. The Num class (simplifying a bit) encompasses all Eq types that also support addition, subtraction, and multiplication. In addition to making overloading a bit more explicit than it is in most languages, type classes make it possible to specify that certain polymorphic functions can be used only when their arguments are of a type that supports some particular overloaded function (for more on this subject, see Sidebar 7.7).
在考虑函数和子程序调用时,区分重载与强制和多态的相关概念非常重要。在某些情况下,这三种方法都可用于将多种类型的参数传递给看似单个命名的程序(或从中返回多种类型的值)。然而,语法相似性隐藏了语义和语用方面的显著差异。
When considering function and subroutine calls, it is important to distinguish overloading from the related concepts of coercion and polymorphism. All three can be used, in certain circumstances, to pass arguments of multiple types to (or return values of multiple types from) what appears to be a single named routine. The syntactic similarity, however, hides significant differences in semantics and pragmatics.
强制转换(我们将在7.2.2 节中详细介绍)是指当周围上下文需要第二种类型时,编译器会自动将一种类型的值转换为另一种类型的值的过程。多态性(我们将在7.1.2 、7.3、10.1.1和14.4.4节中讨论)允许单个子程序接受多种类型的参数。
Coercion, which we will cover in more detail in Section 7.2.2, is the process by which a compiler automatically converts a value of one type into a value of another type when that second type is required by the surrounding context. Polymorphism, which we will consider in Sections 7.1.2,7.3,10.1.1, and 14.4.4, allows a single subroutine to accept arguments of multiple types.
简而言之,重载允许程序员为多个对象赋予相同的名称,并根据上下文(对于子例程,则根据参数的数量或类型)消除歧义(解析)。强制允许编译器执行自动类型转换,使参数符合某些现有例程的预期类型。多态性允许单个例程接受多种类型的参数,前提是它仅尝试以其类型支持的方式使用它们。
In short, overloading allows the programmer to give the same name to multiple objects, and to disambiguate (resolve) them based on context—for subroutines, on the number or types of arguments. Coercion allows the compiler to perform an automatic type conversion to make an argument conform to the expected type of some existing routine. Polymorphism allows a single routine to accept arguments of multiple types, provided that it attempts to use them only in ways that their types support.
我们已经在3.3 节中看到了范围规则如何确定程序中给定语句的引用环境。静态范围规则指定引用环境取决于声明名称的程序块的词汇嵌套。动态范围规则指定引用环境取决于运行时遇到声明的顺序。我们尚未考虑的另一个问题出现在允许创建对子例程的引用(例如,通过将其作为参数传递)的语言中。何时应将范围规则应用于此类子例程:首次创建引用时,还是最终调用例程时?答案对于具有动态范围的语言尤其重要,尽管我们将看到即使在具有静态范围的语言中,答案也很重要。
We have seen in Section 3.3 how scope rules determine the referencing environment of a given statement in a program. Static scope rules specify that the referencing environment depends on the lexical nesting of program blocks in which names are declared. Dynamic scope rules specify that the referencing environment depends on the order in which declarations are encountered at run time. An additional issue that we have not yet considered arises in languages that allow one to create a reference to a subroutine—for example, by passing it as a parameter. When should scope rules be applied to such a subroutine: when the reference is first created, or when the routine is finally called? The answer is particularly important for languages with dynamic scoping, though we shall see that it matters even in languages with static scoping.
“语用学” — 2015/11/2 — 19:18 — 第 153 页 — #183
“pragmatics” — 2015/11/2 — 19:18 — page 153 — #183
深度绑定是通过创建引用环境(通常是当前调用子程序时执行的环境)的显式表示并将其与对子程序的引用捆绑在一起来实现的。整个捆绑包称为闭包。通常,子程序本身可以在闭包中通过指向其代码的指针来表示。在具有动态作用域的语言中,引用环境的表示取决于语言实现是使用关联列表还是中央引用表来进行运行时查找名称;我们将在 C-3.4.2 节末尾考虑这些替代方案。
Deep binding is implemented by creating an explicit representation of a referencing environment (generally the one in which the subroutine would execute if called at the present time) and bundling it together with a reference to the subroutine. The bundle as a whole is referred to as a closure. Usually the subroutine itself can be represented in the closure by a pointer to its code. In a language with dynamic scoping, the representation of the referencing environment depends on whether the language implementation uses an association list or a central reference table for run-time lookup of names; we consider these alternatives at the end of Section C-3.4.2.
在使用动态作用域的早期 Lisp 方言中,深度绑定可通过内置的原始函数获得,该函数将函数作为参数并返回一个闭包,该闭包的引用环境是函数在当时调用时将执行的环境。然后可以将闭包作为参数传递给另一个函数。如果最终调用它,它将在保存的环境中执行。(闭包的工作方式与大多数 Lisp 方言中的“裸”函数略有不同:必须通过将它们传递给内置原始函数funcall或apply 来调用它们。)
In early dialects of Lisp, which used dynamic scoping, deep binding was available via the built-in primitive function, which took a function as its argument and returned a closure whose referencing environment was the one in which the function would have executed if called at that moment in time. The closure could then be passed as a parameter to another function. If and when it was eventually called, it would execute in the saved environment. (Closures work slightly differently from “bare” functions in most Lisp dialects: they must be called by passing them to the built-in primitives funcall or apply.)
乍一看,人们可能会认为在具有静态作用域的语言中,引用环境的绑定时间并不重要。毕竟,静态作用域名称的含义取决于其词汇嵌套,而不是执行流程,并且无论是在将子程序作为参数传递时捕获,还是在调用子程序时捕获,这种嵌套都是相同的。问题在于,正在运行的程序可能具有在递归子程序中声明的对象的多个实例。具有静态作用域的语言中的闭包在创建闭包时捕获每个对象的当前实例。当调用闭包的子程序时,它将找到这些捕获的实例,即使随后通过递归调用创建了较新的实例。
At first glance, one might be tempted to think that the binding time of referencing environments would not matter in a language with static scoping. After all, the meaning of a statically scoped name depends on its lexical nesting, not on the flow of execution, and this nesting is the same whether it is captured at the time a subroutine is passed as a parameter or at the time the subroutine is called. The catch is that a running program may have more than one instance of an object that is declared within a recursive subroutine. A closure in a language with static scoping captures the current instance of every object, at the time the closure is created. When the closure's subroutine is called, it will find these captured instances, even if newer instances have subsequently been created by recursive calls.
应该注意的是,绑定规则仅在访问既非本地也非全局、但在某个中间嵌套级别定义的对象时才与静态作用域有关。如果对象是当前执行的子例程的本地对象,那么子例程是直接调用还是通过闭包调用并不重要;无论是哪种情况,在子例程开始运行时都会创建本地对象。如果对象是全局的,则永远不会有多个实例,因为程序的主体不是递归的。因此,绑定规则与 C 等语言无关,因为 C 语言没有嵌套的子例程,或者 Modula-2 只允许将最外层的子例程作为参数传递,从而确保在子例程之外定义的任何变量都是全局的。(绑定规则与 PL/I 和 Ada 83 等语言无关,因为这些语言根本不允许将子例程作为参数传递。)
It should be noted that binding rules matter with static scoping only when accessing objects that are neither local nor global, but are defined at some intermediate level of nesting. If an object is local to the currently executing subroutine, then it does not matter whether the subroutine was called directly or through a closure; in either case local objects will have been created when the subroutine started running. If an object is global, there will never be more than one instance, since the main body of the program is not recursive. Binding rules are therefore irrelevant in languages like C, which has no nested subroutines, or Modula-2, which allows only outermost subroutines to be passed as parameters, thus ensuring that any variable defined outside the subroutine is global. (Binding rules are also irrelevant in languages like PL/I and Ada 83, which do not permit subroutines to be passed as parameters at all.)
那么假设我们有一种具有静态作用域的语言,其中嵌套的子例程可以作为参数传递,并具有深度绑定。为了表示子例程S的闭包,我们可以简单地将指向S代码的指针与静态链接一起保存,如果现在在当前环境中调用该链接, S将使用它。当最终调用S时,我们会暂时恢复保存的静态链接,而不是创建一个新的链接。当S沿着其静态链访问非本地对象时,它将找到创建闭包时当前的对象实例。这个实例可能没有创建闭包时的值,但它的身份至少会反映闭包创建者的意图。
Suppose then that we have a language with static scoping in which nested subroutines can be passed as parameters, with deep binding. To represent a closure for subroutine S, we can simply save a pointer to S's code together with the static link that S would use if it were called right now, in the current environment. When S is finally called, we temporarily restore the saved static link, rather than creating a new one. When S follows its static chain to access a nonlocal object, it will find the object instance that was current at the time the closure was created. This instance may not have the value it had at the time the closure was created, but its identity, at least, will reflect the intent of the closure's creator.
一般来说,如果编程语言中的值可以作为参数传递、从子程序返回或赋值给变量,则称该值具有一等地位。在大多数编程语言中,整数和字符等简单类型都是一等值。相反,第二类值可以作为参数传递,但不能从子程序返回或赋值给变量,第三类值甚至不能作为参数传递。正如我们将在第9.3.2 节中看到的那样,标签(在具有标签的语言中)通常是第三类值,但在 Algol 中它们是第二类值。子程序表现出最多的变化。它们是所有函数式编程语言和大多数脚本语言中的第一类值。它们在 C# 中也是一等值,并且在其他几种命令式语言中(有一些限制)也是一等值,包括 Fortran、Modula-2 和 -3、Ada 95、C 和 C++。10在大多数其他命令式语言中,它们是二等值,而在 Ada 83 中,它们是三等值。
In general, a value in a programming language is said to have first-class status if it can be passed as a parameter, returned from a subroutine, or assigned into a variable. Simple types such as integers and characters are first-class values in most programming languages. By contrast, a “second-class” value can be passed as a parameter, but not returned from a subroutine or assigned into a variable, and a “third-class” value cannot even be passed as a parameter. As we shall see in Section 9.3.2, labels (in languages that have them) are usually third-class values, but they are second-class values in Algol. Subroutines display the most variation. They are first-class values in all functional programming languages and most scripting languages. They are also first-class values in C# and, with some restrictions, in several other imperative languages, including Fortran, Modula-2 and -3, Ada 95, C, and C++.10 They are second-class values in most other imperative languages, and third-class values in Ada 83.
如果在每个作用域执行结束时销毁本地对象(并回收其空间),那么在长寿命闭包中捕获的引用环境可能会充满悬垂引用。 为了避免这个问题,大多数函数式语言指定本地对象具有无限范围:它们的生命周期无限期地持续下去。 只有当垃圾收集系统能够证明它们永远不会再使用时,才能回收它们的空间。大多数命令式语言中的本地对象(除自身/静态变量外)具有有限的范围:它们在其作用域执行结束时被销毁。 (C# 和 Smalltalk 是该规则的例外,大多数脚本语言也是如此。)可以在堆栈上分配具有有限范围的本地对象的空间。 通常必须在堆上分配具有无限范围的本地对象的空间。
If local objects were destroyed (and their space reclaimed) at the end of each scope's execution, then the referencing environment captured in a long-lived closure might become full of dangling references. To avoid this problem, most functional languages specify that local objects have unlimited extent: their lifetimes continue indefinitely. Their space can be reclaimed only when the garbage collection system is able to prove that they will never be used again. Local objects (other than own/static variables) in most imperative languages have limited extent: they are destroyed at the end of their scope's execution. (C# and Smalltalk are exceptions to the rule, as are most scripting languages.) Space for local objects with limited extent can be allocated on a stack. Space for local objects with unlimited extent must generally be allocated on a heap.
由于希望保持子程序局部变量的基于堆栈的分配,具有一等子程序的命令式语言通常必须采用替代机制来避免闭包的悬空引用问题。当然,C 和(Fortran 90 之前的)Fortran 没有嵌套子程序。Modula-2 只允许对最外层子程序创建引用(最外层例程是一等值;嵌套例程是三等值)。Modula-3 允许将嵌套子程序作为参数传递,但只能返回或将最外层例程存储在变量中(最外层例程是一等值;嵌套例程是二等值)。Ada 95 允许返回嵌套例程,但前提是声明嵌套例程的范围等于或大于声明的返回类型的范围。该包含规则虽然比严格必要的规则更为保守(它禁止图 3.14的 Ada 等效规则),但却无法将子程序引用传播到程序中该程序的引用环境未处于活动状态的部分。
Given the desire to maintain stack-based allocation for the local variables of subroutines, imperative languages with first-class subroutines must generally adopt alternative mechanisms to avoid the dangling reference problem for closures. C and (pre-Fortran 90) Fortran, of course, do not have nested subroutines. Modula-2 allows references to be created only to outermost subroutines (outermost routines are first-class values; nested routines are third-class values). Modula-3 allows nested subroutines to be passed as parameters, but only outermost routines to be returned or stored in variables (outermost routines are first-class values; nested routines are second-class values). Ada 95 allows a nested routine to be returned, but only if the scope in which it was declared is the same as, or larger than, the scope of the declared return type. This containment rule, while more conservative than strictly necessary (it forbids the Ada equivalent of Figure 3.14), makes it impossible to propagate a subroutine reference to a portion of the program in which the routine's referencing environment is not active.
在函数式编程语言中,lambda 表达式使将函数作为值进行操作变得容易——以各种方式组合它们以动态创建新函数。这种操作在命令式语言中不太常见,但即使在命令式语言中,lambda 表达式也可以帮助促进代码重用和通用性。一个特别常见的习惯用法是回调——一个传递到库中的子例程,允许库在适当的时候“回调”到主程序中。回调的示例包括传递到排序例程中的比较运算符、用于过滤集合元素的谓词或响应某些未来事件而调用的处理程序(参见第 9.6.2 节)。
In functional programming languages, lambda expressions make it easy to manipulate functions as values—to combine them in various ways to create new functions on the fly. This sort of manipulation is less common in imperative languages, but even there, lambda expressions can help encourage code reuse and generality. One particularly common idiom is the callback—a subroutine, passed into a library, that allows the library to “call back” into the main program when appropriate. Examples of callbacks include a comparison operator passed into a sorting routine, a predicate used to filter elements of a collection, or a handler to be called in response to some future event (see Section 9.6.2).
随着一等子程序的日益流行,lambda 表达式甚至进入了 C++,而 C++ 缺乏垃圾收集功能,并且强调基于堆栈的分配,因此解决变量捕获问题特别困难。所采用的解决方案符合语言的本质,更强调效率和表现力,而不是运行时安全性。
With the increasing popularity of first-class subroutines, lambda expressions have even made their way into C++, where the lack of garbage collection and the emphasis on stack-based allocation make it particularly difficult to solve the problem of variable capture. The adopted solution, in keeping with the nature of the language, stresses efficiency and expressiveness more than run-time safety.
事实证明,强制转换为函数式接口类型是Java 中 lambda 表达式的唯一用途。具体来说,lambda 表达式没有自己的类型:它们不是真正的对象,不能直接操作。它们在变量捕获方面的行为完全由嵌套类的常用规则决定。我们将在第10.2.3 节中更详细地讨论这些规则;目前,只需注意 Java 与 C++ 一样,不支持无限扩展。
As it turns out, coercion to functional interface types is the only use of lambda expressions in Java. In particular, lambda expressions have no types of their own: they are not really objects, and cannot be directly manipulated. Their behavior with respect to variable capture is entirely determined by the usual rules for nested classes. We will consider these rules in more detail in Section 10.2.3; for now, suffice it to note that Java, like C++, does not support unlimited extent.
现代语言和编译器大多已将宏视为过时之物而弃之不用。命名常量是类型安全的且易于实现,而内联子例程(将在9.2.4 节中讨论)几乎提供了参数化宏的所有性能,而没有参数化宏的限制。少数语言(尤其是 Scheme 和 Common Lisp)采用了另一种方法,以安全一致的方式将宏集成到语言中。所谓的卫生宏隐式封装其参数,避免与结合性和优先级发生意外交互。它们会在必要时重命名变量以避免捕获问题,并且可以在任何表达式上下文中使用它们。然而,与子例程不同,它们在语义分析期间会扩展,因此通常不适合无界递归。它们的吸引力在于,与所有宏一样,它们采用未求值的参数,并根据需要对其进行惰性求值。除其他外,这意味着它们保留了我们的 MAX 示例中的多个副作用“陷阱”。延迟求值在此上下文中是一个错误,但有时可以成为一项功能。我们将在第 6.1.5 节(短路布尔求值)、第 9.3.2 节(按名称调用参数)和第 11.5 节(函数式编程语言中的正常顺序求值)中回顾它。
Modern languages and compilers have, for the most part, abandoned macros as an anachronism. Named constants are type-safe and easy to implement, and in-line subroutines (to be discussed in Section 9.2.4) provide almost all the performance of parameterized macros without their limitations. A few languages (notably Scheme and Common Lisp) take an alternative approach, and integrate macros into the language in a safe and consistent way. So-called hygienic macros implicitly encapsulate their arguments, avoiding unexpected interactions with associativity and precedence. They rename variables when necessary to avoid the capture problem, and they can be used in any expression context. Unlike subroutines, however, they are expanded during semantic analysis, making them generally unsuitable for unbounded recursion. Their appeal is that, like all macros, they take unevaluated arguments, which they evaluate lazily on demand. Among other things, this means that they preserve the multiple side effect “gotcha” of our MAX example. Delayed evaluation was a bug in this context, but can sometimes be a feature. We will return to it in Sections 6.1.5 (short-circuit Boolean evaluation), 9.3.2 (call-by-name parameters), and 11.5 (normal-order evaluation in functional programming languages).
由于大多数大型程序都是逐步构建和测试的,并且非常大的程序的编译可能需要几个小时,因此任何旨在支持大型程序的语言都必须提供单独的编译。
Since most large programs are constructed and tested incrementally, and since the compilation of a very large program can be a multihour operation, any language designed to support large programs must provide for separate compilation.
更深入地
IN MORE DEPTH
在配套网站上,我们考虑了模块和单独编译之间的关系。由于模块是为封装而设计的,并且提供了狭窄的接口,因此它们自然而然地成为许多编程语言的“编译单元”的选择。例如,Modula-3 和 Ada 的单独模块头和模块体明确用于单独编译,并反映了在其他语言中使用更原始功能的经验。相比之下,C 和 C++ 必须保持与 20 世纪 70 年代早期设计的机制的向后兼容性。C 和 C++ 的现代版本包含一个提供类似模块的数据隐藏的命名空间机制,但名称在每个编译单元中使用之前仍必须声明,而用于适应此规则的机制纯粹是惯例问题。Java 和 C# 打破了 C 传统,要求编译器从单独编译的类定义中自动推断头信息;不需要头文件。
On the companion site we consider the relationship between modules and separate compilation. Because they are designed for encapsulation and provide a narrow interface, modules are the natural choice for the “compilation units” of many programming languages. The separate module headers and bodies of Modula-3 and Ada, for example, are explicitly intended for separate compilation, and reflect experience gained with more primitive facilities in other languages. C and C++, by contrast, must maintain backward compatibility with mechanisms designed in the early 1970s. Modern versions of C and C++ include a namespace mechanism that provides module-like data hiding, but names must still be declared before they are used in every compilation unit, and the mechanisms used to accommodate this rule are purely a matter of convention. Java and C# break with the C tradition by requiring the compiler to infer header information automatically from separately compiled class definitions; no header files are required.
本章讨论了名称以及名称与对象(广义上)的绑定。我们首先对绑定时间的概念进行了一般性讨论——绑定时间是指名称与特定对象相关联的时间,或者更一般地说,绑定时间是指答案与语言或程序设计或实现中的任何未决问题相关联的时间。我们为对象和名称与对象绑定定义了生存期的概念,并指出它们不必相同。然后,我们介绍了用于管理对象空间的三种主要存储分配机制——静态、堆栈和堆。
This chapter has addressed the subject of names, and the binding of names to objects (in a broad sense of the word). We began with a general discussion of the notion of binding time—the time at which a name is associated with a particular object or, more generally, the time at which an answer is associated with any open question in language or program design or implementation. We defined the notion of lifetime for both objects and name-to-object bindings, and noted that they need not be the same. We then introduced the three principal storage allocation mechanisms—static, stack, and heap—used to manage space for objects.
在第 3.3 节中,我们描述了名称与对象的绑定如何受作用域规则的支配。在某些语言中,作用域规则是动态的:名称的含义位于最近进入的包含声明且尚未退出的作用域中。然而,在大多数现代语言中,作用域规则是静态的或词汇的:名称的含义位于词汇上最接近的包含声明的作用域中。我们发现词汇作用域规则在不同语言之间存在重要但有时微妙的差异。我们考虑了哪些类型的作用域可以嵌套,作用域是开放的还是封闭的,名称的作用域是否包含声明它的整个块,以及名称必须在使用前声明。我们在3.4 节中探讨了范围规则的实现。
In Section 3.3 we described how the binding of names to objects is governed by scope rules. In some languages, scope rules are dynamic: the meaning of a name is found in the most recently entered scope that contains a declaration and that has not yet been exited. In most modern languages, however, scope rules are static, or lexical: the meaning of a name is found in the closest lexically surrounding scope that contains a declaration. We found that lexical scope rules vary in important but sometimes subtle ways from one language to another. We considered what sorts of scopes are allowed to nest, whether scopes are open or closed, whether the scope of a name encompasses the entire block in which it is declared, and whether a name must be declared before it is used. We explored the implementation of scope rules in Section 3.4.
在第 3.5 节中,我们研究了绑定相互关联的几种方式。当给定范围内的两个或多个名称绑定到同一个对象时,就会出现别名。当一个名称绑定到多个对象时,就会出现重载。我们注意到,虽然有时可以通过强制或多态实现类似于重载的行为,但底层机制实际上非常不同。在第 3.6 节中,我们考虑了何时将引用环境绑定到作为参数传递、从函数返回或存储在变量中的子例程的问题。我们的讨论涉及闭包和lambda 表达式的概念,这两者都将在后面的章节中反复出现。在第 3.7和3.8节中,我们考虑了宏和单独编译。
In Section 3.5 we examined several ways in which bindings relate to one another. Aliases arise when two or more names in a given scope are bound to the same object. Overloading arises when one name is bound to multiple objects. We noted that while behavior reminiscent of overloading can sometimes be achieved through coercion or polymorphism, the underlying mechanisms are really very different. In Section 3.6 we considered the question of when to bind a referencing environment to a subroutine that is passed as a parameter, returned from a function, or stored in a variable. Our discussion touched on the notions of closures and lambda expressions, both of which will appear repeatedly in later chapters. In Sections 3.7 and 3.8 we considered macros and separate compilation.
词法作用域的一些更复杂的方面说明了语言对数据抽象支持的演变,我们将在第 10 章中回到这个主题。我们首先描述了 Fortran、Algol 60 和 C 等语言的自身或静态变量,这些变量允许子程序的本地变量在一次调用到下一次调用之间保留其值。然后我们注意到,简单模块可以看作是一种将长寿命对象本地化为一组子程序的方法,这样它们对程序的其他部分就不可见了。通过有选择地导出名称,模块可以充当一种或多种抽象数据类型的“管理器”。在下一个复杂程度下,我们注意到有些语言将模块视为类型,允许程序员创建任意数量的模块定义的抽象实例。最后,我们注意到面向对象语言通过提供继承机制扩展了模块即类型方法(以及词法范围的概念),该机制允许将新的抽象(类)定义为现有类的扩展或改进。
Some of the more complicated aspects of lexical scoping illustrate the evolution of language support for data abstraction, a subject to which we will return in Chapter 10. We began by describing the own or static variables of languages like Fortran, Algol 60, and C, which allow a variable that is local to a subroutine to retain its value from one invocation to the next. We then noted that simple modules can be seen as a way to make long-lived objects local to a group of subroutines, in such a way that they are not visible to other parts of the program. By selectively exporting names, a module may serve as the “manager” for one or more abstract data types. At the next level of complexity, we noted that some languages treat modules as types, allowing the programmer to create an arbitrary number of instances of the abstraction defined by a module. Finally, we noted that object-oriented languages extend the module-as-type approach (as well as the notion of lexical scope) by providing an inheritance mechanism that allows new abstractions (classes) to be defined as extensions or refinements of existing classes.
在本章讨论的主题中,我们看到了几个有用的特性(递归、静态作用域、前向引用、一等子程序、无限范围)的例子,由于担心实现复杂性或运行时成本,某些语言已经省略了这些特性。我们还看到了一个特性的例子(模块规范的私有部分),它是为了方便语言的实现而专门引入的,另一个特性(C 中的单独编译)的设计显然是为了反映特定的实现。在语言设计的其他几个方面(后期绑定与早期绑定、静态作用域与动态作用域、对强制和转换的支持、对指针和其他别名的容忍度),我们看到实现问题起着重要作用。
Among the topics considered in this chapter, we saw several examples of useful features (recursion, static scoping, forward references, first-class subroutines, unlimited extent) that have been omitted from certain languages because of concern for their implementation complexity or run-time cost. We also saw an example of a feature (the private part of a module specification) introduced expressly to facilitate a language's implementation, and another (separate compilation in C) whose design was clearly intended to mirror a particular implementation. In several additional aspects of language design (late vs early binding, static vs dynamic scoping, support for coercions and conversions, toleration of pointers and other aliases), we saw that implementation issues play a major role.
同样,看似简单的语言规则也可能产生意想不到的影响。例如,在第 3.3.3 节中,我们考虑了整个块作用域与名称必须先声明后才能使用的要求之间的相互作用。与 Fortran 的 do 循环语法和空格规则(第 2.2.2 节)或Pascal 的if…then…else语法(第 2.3.2 节)一样,选择不当的作用域规则不仅会使编译器难以进行程序分析,而且会使人类也难以进行程序分析。在后续章节中,我们将看到几个令人困惑且难以编译的功能的其他示例。当然,语义效用和易用性实现并不总是齐头并进。许多易于编译的功能(例如 goto 语句)充其量也只是值得怀疑的价值。我们还将看到几个非常有用且(概念上)简单的功能示例,例如垃圾收集(第 8.5.3 节)和统一(第 7.2.4 节、C-7.3.2 节和12.2.1 节),它们的实现相当复杂。
In a similar vein, apparently simple language rules can have surprising implications. In Section 3.3.3, for example, we considered the interaction of whole-block scope with the requirement that names be declared before they can be used. Like the do loop syntax and white space rules of Fortran (Section 2.2.2) or the if… then …else syntax of Pascal (Section 2.3.2), poorly chosen scoping rules can make program analysis difficult not only for the compiler, but for human beings as well. In future chapters we shall see several additional examples of features that are both confusing and hard to compile. Of course, semantic utility and ease of implementation do not always go together. Many easy-to-compile features (e.g., goto statements) are of questionable value at best. We will also see several examples of highly useful and (conceptually) simple features, such as garbage collection (Section 8.5.3) and unification (Sections 7.2.4, C-7.3.2, and 12.2.1), whose implementations are quite complex.
3.1 指出您最喜欢的编程语言和实现中下列每个决策的约束时间(语言设计时、程序链接时、程序开始执行时等)。解释您认为任何有待解释的答案。
3.1 Indicate the binding time (when the language is designed, when the program is linked, when the program begins execution, etc.) for each of the following decisions in your favorite programming language and implementation. Explain any answers you think are open to interpretation.
■ The number of built-in functions (math, type queries, etc.)
■ 与特定变量引用对应的变量声明(使用)
■ The variable declaration that corresponds to a particular variable reference (use)
■ 常量(文字)字符串的最大长度
■ The maximum length allowed for a constant (literal) character string
■ 作为参数传递的子程序的引用环境
■ The referencing environment for a subroutine that is passed as a parameter
■ 特定库例程的地址
■ The address of a particular library routine
■ 程序代码和数据占用的空间总量
■ The total amount of space occupied by program code and data
3.2 在 Fortran 77 中,局部变量通常是静态分配的。在 Algol 及其后代(例如 Ada 和 C)中,它们通常是在堆栈中分配的。在 Lisp 中,它们通常是至少部分地在堆中分配的。这些差异是如何造成的?举一个 Ada 或 C 程序的例子,如果局部变量是静态分配的,该程序将无法正常工作。举一个 Scheme 或 Common Lisp 程序的例子,如果局部变量是在堆栈中分配的,该程序将无法正常工作。
3.2 In Fortran 77, local variables were typically allocated statically. In Algol and its descendants (e.g., Ada and C), they are typically allocated in the stack. In Lisp they are typically allocated at least partially in the heap. What accounts for these differences? Give an example of a program in Ada or C that would not work correctly if local variables were allocated statically. Give an example of a program in Scheme or Common Lisp that would not work correctly if local variables were allocated on the stack.
3.3 举两个例子,说明推迟执行决定的约束力可能是有意义的,尽管有足够的信息可以尽早执行该决定。
3.3 Give two examples in which it might make sense to delay the binding of an implementation decision, even though sufficient information exists to bind it early.
3.4 从你熟悉的编程语言中给出三个具体的例子,其中变量是有效的但不在范围内。
3.4 Give three concrete examples drawn from programming languages with which you are familiar in which a variable is live but not in scope.
3.5 考虑以下伪代码:
1. 过程 main()
2. a : integer := 1
3. b : integer := 2
4. 过程 middle()
5. b : integer := a
6. 过程 inner()
7. 打印 a, b
8. a : integer := 3
9. –– 中间部分
10. inner()
11. 打印 a, b
12. –– 主部分
13. middle()
14. 打印 a, b
假设这是具有 C 的声明顺序规则(但具有嵌套子例程)的语言的代码 - 即,名称必须在使用前声明,并且名称的范围从其声明延伸到块的末尾。在每个打印语句中,指出a和b的哪些声明在引用环境中。程序会打印什么(或者编译器会识别静态语义错误)?重复练习 C# 的声明顺序规则(名称必须在使用前声明,但名称的范围是声明它的整个块)和 Modula-3(名称可以按任何顺序声明,并且它们的范围是声明它们的整个块)。
3.5 Consider the following pseudocode:
1. procedure main()
2. a : integer := 1
3. b : integer := 2
4. procedure middle()
5. b : integer := a
6. procedure inner()
7. print a, b
8. a : integer := 3
9. – – body of middle
10. inner()
11. print a, b
12. –– body of main
13. middle()
14. print a, b
Suppose this was code for a language with the declaration-order rules of C (but with nested subroutines)—that is, names must be declared before use, and the scope of a name extends from its declaration through the end of the block. At each print statement, indicate which declarations of a and b are in the referencing environment. What does the program print (or will the compiler identify static semantic errors)? Repeat the exercise for the declaration-order rules of C# (names must be declared before use, but the scope of a name is the entire block in which it is declared) and of Modula-3 (names can be declared in any order, and their scope is the entire block in which they are declared).
3.6 考虑以下伪代码,假设嵌套子程序和静态范围:过程 main() g : 整数过程 B(a : 整数) x : 整数过程 A(n : 整数) g := n过程 R(m : 整数) write_integer(x) x /:= 2 ––如果 x > 1 ,则为整数除法R(m + 1)否则A(m) –– B 的主体x := a × a R(1) –– main 的主体B(3) write_integer(g)
3.6 Consider the following pseudocode, assuming nested subroutines and static scope:
procedure main()
g : integer
procedure B(a : integer)
x : integer
procedure A(n : integer)
g := n
procedure R(m : integer)
write_integer(x)
x /:= 2 –– integer division
if x > 1
R(m + 1)
else
A(m)
–– body of B
x := a × a
R(1)
–– body of main
B(3)
write_integer(g)
(a) What does this program print?
(b)显示 A刚被调用时堆栈上的框架。对于每个框架,显示静态和动态链接。
(b) Show the frames on the stack when A has just been called. For each frame, show the static and dynamic links.
(c) 解释A如何找到g。
(c) Explain how A finds g.
3.7 作为 MumbleTech.com 开发团队的一员,Janet 编写了一个 C 语言列表操作库,其中包含图 3.16中的代码。
3.7 As part of the development team at MumbleTech.com, Janet has written a list manipulation library for C that contains, among other things, the code in Figure 3.16.
(a) 新团队成员 Brad 习惯于 Java,他在程序的主循环中包含以下代码:list_node* L = 0; while (more_widgets()) { L = insert(next_widget(), L); } L = reverse(L);
遗憾的是,运行一段时间后,Brad 的程序总是内存不足并崩溃。请解释一下问题出在哪里。
(a) Accustomed to Java, new team member Brad includes the following code in the main loop of his program:
list_node* L = 0;
while (more_widgets()) {
L = insert(next_widget(), L);
}
L = reverse(L);
Sadly, after running for a while, Brad's program always runs out of memory and crashes. Explain what's going wrong.
(b) 在 Janet 耐心地向 Brad 解释完问题后,Brad 又尝试了一次:list_node* L = 0; while (more_widgets()) { L = insert(next_widget(), L); } list_node* T = reverse(L); delete_list(L); L = T;这似乎解决了内存不足的问题,但是程序过去会产生正确的结果(在内存耗尽之前),现在其输出却奇怪地损坏了,Brad 又回到 Janet 那里寻求建议。这次她会告诉他什么呢?
(b) After Janet patiently explains the problem to him, Brad gives it another try:
list_node* L = 0;
while (more_widgets()) {
L = insert(next_widget(), L);
}
list_node* T = reverse(L);
delete_list(L);
L = T;
This seems to solve the insufficient memory problem, but where the program used to produce correct results (before running out of memory), now its output is strangely corrupted, and Brad goes back to Janet for advice. What will she tell him this time?
3.8用 C 语言 重写图 3.6和3.7。您需要使用单独的编译来隐藏名称。
3.8 Rewrite Figures 3.6 and 3.7 in C. You will need to use separate compilation for name hiding.
3.9 考虑以下 C 语言代码片段:
{ int a, b, c; … { int d, e; … { int f; … } … } … { int g, h, i; … } … }
3.9 Consider the following fragment of code in C:
{ int a, b, c;
…
{ int d, e;
…
{ int f;
…
}
…
}
…
{ int g, h, i;
…
}
…
}
(a) 假设每个整型变量占用四个字节。这段代码中的变量总共需要多少空间?
(a) Assume that each integer variable occupies four bytes. How much total space is required for the variables in this code?
(b) 描述一种算法,编译器可以使用该算法将堆栈帧偏移量分配给任意嵌套块的变量,以最小化所需的总空间。
(b) Describe an algorithm that a compiler could use to assign stack frame offsets to the variables of arbitrary nested blocks, in a way that minimizes the total space required.
3.10 考虑 Fortran 77 编译器的设计,该编译器对子例程的局部变量使用静态分配。扩展上一个问题的答案,描述一种算法来最小化这些变量所需的总空间。您可能会发现构建调用图数据很有帮助结构中每个节点代表一个子程序,每条有向弧表示尾部的子程序有时可能会调用头部的子程序。
3.10 Consider the design of a Fortran 77 compiler that uses static allocation for the local variables of subroutines. Expanding on the solution to the previous question, describe an algorithm to minimize the total space required for these variables. You may find it helpful to construct a call graph data structure in which each node represents a subroutine, and each directed arc indicates that the subroutine at the tail may sometimes call the subroutine at the head.
3.11 考虑以下伪代码:
过程 P(A, B : real) X : real过程 Q(B, C : real) Y : real …过程 R(A, C : real) Z : real … –– (*) …假设静态范围,(*) 标记位置的引用环境是什么?
3.11 Consider the following pseudocode:
procedure P(A, B : real)
X : real
procedure Q(B, C : real)
Y : real
…
procedure R(A, C : real)
Z : real
… –– (*)
…
Assuming static scope, what is the referencing environment at the location marked by (*)?
3.12 用 Scheme 编写一个简单的程序,根据我们使用let、let*还是letrec来声明给定的名称集,显示三种不同的行为。(提示:为了充分利用letrec,你可能希望你的名字是函数 [ lambda表达式]。)
3.12 Write a simple program in Scheme that displays three different behaviors, depending on whether we use let, let*, or letrec to declare a given set of names. (Hint: To make good use of letrec, you will probably want your names to be functions [lambda expressions].)
3.13 考虑以下 Scheme 程序:
(define A (lambda() (let* ((x 2) (C (lambda (P) (let ((x 4)) (P)))) (D (lambda () x)) (B (lambda () (let ((x 3)) (CD))))) (B))))该程序打印什么?如果 Scheme 使用动态作用域和浅绑定,它会打印什么?动态作用域和深绑定?解释你的答案。
3.13 Consider the following program in Scheme:
(define A
(lambda()
(let* ((x 2)
(C (lambda (P)
(let ((x 4))
(P))))
(D (lambda ()
x))
(B (lambda ()
(let ((x 3))
(C D)))))
(B))))
What does this program print? What would it print if Scheme used dynamic scoping and shallow binding? Dynamic scoping and deep binding? Explain your answers.
3.14 考虑以下伪代码:
x : 整数 –– 全局
过程集 x(n : 整数) x := n过程 print_x()写入整数(x)过程 first() set_x(1) print_x()过程 second() x : 整数set_x(2) print_x() set_x(0) first() print_x() second() print_x()
如果语言使用静态作用域,该程序会打印什么?如果使用动态作用域,它会打印什么?为什么?
3.14 Consider the following pseudocode:
x : integer –– global
procedure set x(n : integer)
x := n
procedure print_x()
write integer(x)
procedure first()
set_x(1)
print_x()
procedure second()
x : integer
set_x(2)
print_x()
set_x(0)
first()
print_x()
second()
print_x()
What does this program print if the language uses static scoping? What does it print with dynamic scoping? Why?
3.15 支持动态作用域的主要论点是它有助于定制子例程。例如,假设我们有一个库例程print_integer,它能够以几种进制(十进制、二进制、十六进制等)中的任意一种打印其参数。进一步假设我们希望例程大多数时候都使用十进制表示法,并且只在少数特殊情况下使用其他进制:我们不想在每次单独调用时都明确指定进制。我们可以通过动态作用域来实现此结果,方法是让print_integer从非局部变量print_base获取其进制。我们可以通过在执行初期遇到的作用域中声明变量print_base并将其值设置为 10 来建立默认行为。然后,任何时候我们想要临时更改基数,我们都可以这样写:
begin – – 嵌套块print_base : integer := 16 – – 使用十六进制print_integer(n)此参数的问题在于,通常还有其他方法可以实现相同的效果,而无需动态作用域。请至少描述两个print_integer示例。
3.15 The principal argument in favor of dynamic scoping is that it facilitates the customization of subroutines. Suppose, for example, that we have a library routine print_integer that is capable of printing its argument in any of several bases (decimal, binary, hexadecimal, etc.). Suppose further that we want the routine to use decimal notation most of the time, and to use other bases only in a few special cases: we do not want to have to specify a base explicitly on each individual call. We can achieve this result with dynamic scoping by having print_integer obtain its base from a nonlocal variable print_base. We can establish the default behavior by declaring a variable print_base and setting its value to 10 in a scope encountered early in execution. Then, any time we want to change the base temporarily, we can write
begin – – nested block
print_base : integer := 16 – – use hexadecimal
print_integer(n)
The problem with this argument is that there are usually other ways to achieve the same effect, without dynamic scoping. Describe at least two for the print_integer example.
3.16如 第 3.6.3 节所述,C# 对一流子例程的支持异常复杂。除其他外,它允许从匿名嵌套方法实例化委托,并在此类委托可能需要局部变量和参数时为其提供无限范围。考虑以下 C# 程序中这些功能的含义:
using System;
public delegate int UnaryOp(int n); // 类型声明:UnaryOp 是一个从 int 到 int 的函数public class Foo { static int a = 2; static UnaryOp b(int c) { int d = a + c; Console.WriteLine(d); return delegate(int n) { return c + n; }; } public static void Main(string[] args) { Console.WriteLine(b(3)(4)); } }
这个程序打印了什么?a、b、c和d中的哪一个(如果有的话)可能被静态分配?哪一个可以在堆栈上分配?哪一个需要在堆上分配?解释一下。
3.16 As noted in Section 3.6.3, C# has unusually sophisticated support for first-class subroutines. Among other things, it allows delegates to be instantiated from anonymous nested methods, and gives local variables and parameters unlimited extent when they may be needed by such a delegate. Consider the implications of these features in the following C# program:
using System;
public delegate int UnaryOp(int n);
// type declaration: UnaryOp is a function from ints to ints
public class Foo {
static int a = 2;
static UnaryOp b(int c) {
int d = a + c;
Console.WriteLine(d);
return delegate(int n) { return c + n; };
}
public static void Main(string[] args) {
Console.WriteLine(b(3)(4));
}
}
What does this program print? Which of a, b, c, and d, if any, is likely to be statically allocated? Which could be allocated on the stack? Which would need to be allocated in the heap? Explain.
3.17 如果您熟悉结构化异常处理(如 Ada、C++、Java、C#、ML、Python 或 Ruby 中提供),请考虑此机制与范围问题的关系。通常,raise或throw语句被认为是引用异常,它将异常作为参数传递给处理程序查找库例程。在上述每种语言中,异常本身都必须在某个周围范围内声明,并遵守通常的静态范围规则。描述另一种观点,其中 raise或throw实际上是对处理程序的引用,它将控制权直接转移到该处理程序。从这个角度来看,处理程序的范围规则是什么?这些规则与语言的其余部分一致吗?解释一下。(有关异常的更多信息,请参见第 9.4 节。)
3.17 If you are familiar with structured exception handling, as provided in Ada, C++, Java, C#, ML, Python, or Ruby, consider how this mechanism relates to the issue of scoping. Conventionally, a raise or throw statement is thought of as referring to an exception, which it passes as a parameter to a handler-finding library routine. In each of the languages mentioned, the exception itself must be declared in some surrounding scope, and is subject to the usual static scope rules. Describe an alternative point of view, in which the raise or throw is actually a reference to a handler, to which it transfers control directly. Assuming this point of view, what are the scope rules for handlers? Are these rules consistent with the rest of the language? Explain. (For further information on exceptions, see Section 9.4.)
3.18 考虑以下伪代码:
x : 整数 – – 全局
过程 set_x(n : 整数) x := n过程 print_x() write_integer(x)过程 foo(S, P : 函数; n : 整数) x : 整数 := 5 if n in {1, 3} set_x(n) else S(n) if n in {1, 2} print_x() else P set_x(0); foo(set_x, print_x, 1); print_x() set_x(0); foo(set_x, print_x, 2); print_x() set_x(0); foo(set_x, print_x, 3); print_x() set_x(0); foo(set_x, print_x, 4); print_x()
假设该语言使用动态作用域。如果该语言使用浅绑定,程序会打印什么?如果使用深绑定,程序会打印什么?为什么?
3.18 Consider the following pseudocode:
x : integer – – global
procedure set_x(n : integer)
x := n
procedure print_x()
write_integer(x)
procedure foo(S, P : function; n : integer)
x : integer := 5
if n in {1, 3}
set_x(n)
else
S(n)
if n in {1, 2}
print_x()
else
P
set_x(0); foo(set_x, print_x, 1); print_x()
set_x(0); foo(set_x, print_x, 2); print_x()
set_x(0); foo(set_x, print_x, 3); print_x()
set_x(0); foo(set_x, print_x, 4); print_x()
Assume that the language uses dynamic scoping. What does the program print if the language uses shallow binding? What does it print with deep binding? Why?
3.19 考虑以下伪代码:
x : integer := 1
y : integer := 2
procedure add() x := x + y procedure second(P : procedure) x : integer := 2 P() procedure first y : integer := 3 second(add) first() write_integer(x)
3.19 Consider the following pseudocode:
x : integer := 1
y : integer := 2
procedure add()
x := x + y
procedure second(P : procedure)
x : integer := 2
P()
procedure first
y : integer := 3
second(add)
first()
write_integer(x)
(a) What does this program print if the language uses static scoping?
(b) 如果该语言使用具有深度绑定的动态作用域,它会打印什么?
(b) What does it print if the language uses dynamic scoping with deep binding?
(c) 如果该语言使用动态作用域和浅绑定,它会打印什么?
(c) What does it print if the language uses dynamic scoping with shallow binding?
3.20 考虑像 C++ 这样的语言中的数学运算,它既支持重载也支持强制转换。在许多情况下,为一个函数提供多个重载版本是有意义的,每个版本对应一个数值类型或多个类型的组合。在其他情况下,我们可能会使用单个版本(可能为双精度浮点参数定义),并依靠强制转换允许该函数用于其他数值类型(例如整数)。举一个例子,其中重载显然是首选方法。再举一个例子,其中强制转换几乎肯定更好。
3.20 Consider mathematical operations in a language like C++, which supports both overloading and coercion. In many cases, it may make sense to provide multiple, overloaded versions of a function, one for each numeric type or combination of types. In other cases, we might use a single version—probably defined for double-precision floating point arguments—and rely on coercion to allow that function to be used for other numeric types (e.g., integers). Give an example in which overloading is clearly the preferable approach. Give another in which coercion is almost certainly better.
3.21 在支持运算符重载的语言中,构建对有理数的支持。每个数字应在内部以最简单形式表示为(分子,分母)对,分母为正。您的代码应支持一元否定和四个标准算术运算符。为了获得额外分数,请创建一个转换例程,该例程接受两个浮点参数(一个值和一个错误界限),并返回给定值的给定错误界限内最简单(分母最小)的有理数。
3.21 In a language that supports operator overloading, build support for rational numbers. Each number should be represented internally as a (numerator, denominator) pair in simplest form, with a positive denominator. Your code should support unary negation and the four standard arithmetic operators. For extra credit, create a conversion routine that accepts two floating-point parameters—a value and a error bound—and returns the simplest (smallest denominator) rational number within the given error bound of the given value.
3.22 在具有 lambda 表达式的命令式语言(例如 C#、Ruby、C++ 或 Java)中,编写以下高级函数。(我们将在第 11 章中看到,高级函数将其他函数作为参数和/或返回一个函数作为结果。)
3.22 In an imperative language with lambda expressions (e.g., C#, Ruby, C++, or Java), write the following higher-level functions. (A higher-level function, as we shall see in Chapter 11, takes other functions as argument and/or returns a function as a result.)
■ compose(g, f) — 返回一个函数 h,使得h(x) ==g(f(x))。
■ compose(g, f)—returns a function h such that h(x) ==g(f(x)).
■ map(f, L) — 给定一个函数f和一个列表L,返回一个列表M,使得M的第 i 个元素是f( e ),其中e是L的第 i 个元素。
■ map(f, L)—given a function f and a list L returns a list M such that the ith element of M is f(e), where e is the ith element of L.
■ filter(L, P) — 给定一个列表L和一个谓词(布尔返回函数)P ,返回一个列表,该列表包含L中所有且仅包含P为真的元素。
■ filter(L, P)—given a list L and a predicate (Boolean-returning function) P, returns a list containing all and only those elements of L for which P is true.
理想情况下,您的代码应该适用于任何参数或列表元素类型。
Ideally, your code should work for any argument or list element type.
3.23 能否用标准 C 编写一个宏,在不调用子程序的情况下“返回”一对参数的最大公约数?为什么可以或为什么不可以?
3.23 Can you write a macro in standard C that “returns” the greatest common divisor of a pair of arguments, without calling a subroutine? Why or why not?
3.24–3.31 更深入。
3.24–3.31 In More Depth.
3.32 用你最喜欢的编程语言试验命名规则。阅读手册,编写并编译一些测试程序。该语言使用词法作用域还是动态作用域?作用域可以嵌套吗?它们是开放的还是封闭的?名称的作用域是否包含声明它的整个块,还是仅包含声明后的部分?如何声明相互递归的类型或子程序?子程序可以作为参数传递、从函数返回或存储在变量中吗?如果可以,引用环境何时绑定?
3.32 Experiment with naming rules in your favorite programming language. Read the manual, and write and compile some test programs. Does the language use lexical or dynamic scoping? Can scopes nest? Are they open or closed? Does the scope of a name encompass the entire block in which it is declared, or only the portion after the declaration? How does one declare mutually recursive types or subroutines? Can subroutines be passed as parameters, returned from functions, or stored in variables? If so, when are referencing environments bound?
3.33 列出一种或多种编程语言的关键字(保留字)。列出预定义标识符。(回想一下,每个关键字都是一个单独的标记。标识符不能与关键字具有相同的拼写。)您认为使用什么标准来决定哪些名称应该是关键字,哪些应该是预定义标识符?您是否同意这些选择?为什么或为什么不?
3.33 List the keywords (reserved words) of one or more programming languages. List the predefined identifiers. (Recall that every keyword is a separate token. An identifier cannot have the same spelling as a keyword.) What criteria do you think were used to decide which names should be keywords and which should be predefined identifiers? Do you agree with the choices? Why or why not?
3.34 如果你有使用 C、C++ 或 Rust 等语言的经验,其中动态分配的空间必须手动回收,请描述你在处理悬垂引用或内存泄漏方面的经验。这些情况发生的频率是多少错误出现了吗?你如何找到它们?需要付出多少努力?了解用于查找存储错误的开源或商业工具(Valgrind是一个流行的开源示例)。这些工具是否会削弱自动垃圾收集的论点?
3.34 If you have experience with a language like C, C++, or Rust, in which dynamically allocated space must be manually reclaimed, describe your experience with dangling references or memory leaks. How often do these bugs arise? How do you find them? How much effort does it take? Learn about open-source or commercial tools for finding storage bugs (Valgrind is a popular open-source example). Do such tools weaken the argument for automatic garbage collection?
3.35 一些语言(尤其是 Euclid 和 Turing)将每个子程序都设为封闭范围,并要求它显式导入它使用的任何非本地名称。导入列表可以被认为是子程序接口中通常隐式的部分的显式、强制文档。使用导入列表还使 Euclid 和 Turing 能够轻松禁止将变量通过引用传递给也直接访问该变量的子程序,从而避免示例 3.20中提到的错误。
在您编写的程序中,记录非本地变量的每次使用有多难?为提高文档质量和错误率而付出的努力是否值得?
3.35 A few languages—notably Euclid and Turing, make every subroutine a closed scope, and require it to explicitly import any nonlocal names it uses. The import lists can be thought of as explicit, mandatory documentation of a part of the subroutine interface that is usually implicit. The use of import lists also makes it easy for Euclid and Turing to prohibit passing a variable, by reference, to a subroutine that also accesses that variable directly, thereby avoiding the errors alluded to in Example 3.20.
In programs you have written, how hard would it have been to document every use of a nonlocal variable? Would the effort be worth the improvement in the quality of documentation and error rates?
3.36我们在 3.3.6 节中了解到,现代语言通常已经放弃了动态作用域。仍然可以在Unix 编程环境中的所谓环境变量中找到它。如果您不熟悉这些变量,请阅读您最喜欢的 shell(命令解释器— ksh/bash、csh/tcsh等)的手册页,以了解它们的行为方式。解释为什么动态作用域的常用替代方案(默认参数和静态变量)在这种情况下不合适。
3.36 We learned in Section 3.3.6 that modern languages have generally abandoned dynamic scoping. One place it can still be found is in the so-called environment variables of the Unix programming environment. If you are not familiar with these, read the manual page for your favorite shell (command interpreter—ksh/bash, csh/tcsh, etc.) to learn how these behave. Explain why the usual alternatives to dynamic scoping (default parameters and static variables) are not appropriate in this case.
3.37 比较 Ada 和 Modula-3 或 C# 中枚举名称的重载机制(第 3.5.2 节)。有人可能会认为(历史上较新的)Modula-3/C# 方法将责任从编译器转移到程序员:它要求即使明确使用枚举常量也要用其类型注释。您认为语言设计者为什么选择这种方法?您同意这个选择吗?为什么或为什么不?
3.37 Compare the mechanisms for overloading of enumeration names in Ada and in Modula-3 or C# (Section 3.5.2). One might argue that the (historically more recent) Modula-3/C# approach moves responsibility from the compiler to the programmer: it requires even an unambiguous use of an enumeration constant to be annotated with its type. Why do you think this approach was chosen by the language designers? Do you agree with the choice? Why or why not?
3.38 了解Perl 中的绑定变量。这些变量允许程序员将普通变量与(面向对象的)对象关联起来,这样对变量的操作就会自动解释为对对象的方法调用。例如,假设我们写了tie $my_var,“ my_class ”;。解释器将创建一个新的my_class类对象,并将其与标量变量 $ my_var关联。为了便于讨论,将该对象称为O 。现在,任何读取$my_var值的尝试都将被解释为对方法O -> FETCH()的调用。类似地,赋值$my_var = value将被解释为对O -> STORE ( value )的调用。数组、哈希和文件句柄变量支持一组更大的内置操作,在绑定时提供对一组更大的方法的访问。
将 Perl 的绑定机制与 C++ 的运算符重载进行比较。每种语言的哪些特性可以方便地被另一种语言模拟?
3.38 Learn about tied variables in Perl. These allow the programmer to associate an ordinary variable with an (object-oriented) object in such a way that operations on the variable are automatically interpreted as method invocations on the object. As an example, suppose we write tie $my_var, “my_class“;. The interpreter will create a new object of class my_class, which it will associate with scalar variable $my_var. For purposes of discussion, call that object O. Now, any attempt to read the value of $my_var will be interpreted as a call to method O->FETCH(). Similarly, the assignment $my_var = value will be interpreted as a call to O->STORE(value). Array, hash, and filehandle variables, which support a larger set of built-in operations, provide access to a larger set of methods when tied.
Compare Perl's tying mechanism to the operator overloading of C++. Which features of each language can be conveniently emulated by the other?
3.39 你认为强制措施是个好主意吗?为什么?
3.39 Do you think coercion is a good idea? Why or why not?
3.40 Ruby 中 lambda 表达式的语法随着时间的推移而不断发展,现在有四种方法可以将块作为闭包传递到方法中:将其放在参数列表末尾(在这种情况下,它将成为额外的最终参数);将其传递给Proc.new;或者,在参数列表中,在其前面加上关键字 lambda 或将其写入 -> lambda 符号。研究这些选项。哪个先出现?哪个后来出现?它们的比较优势是什么?它们的行为是否存在细微差别?
3.40 The syntax for lambda expressions in Ruby evolved over time, with the result that there are now four ways to pass a block into a method as a closure: by placing it after the end of the argument list (in which case it become an extra, final parameter); by passing it to Proc.new; or, within the argument list, by prefixing it with the keyword lambda or by writing it in -> lambda notation. Investigate these options. Which came first? Which came later? What are their comparative advantages? Are their any minor differences in their behavior?
3.41 Lambda 表达式是 Java 编程语言的后期添加的:多年来一直受到强烈抵制。研究围绕它们的争议。你支持哪一种?哪些替代方案被拒绝了?你觉得其中有什么有吸引力吗?
3.41 Lambda expressions were a late addition to the Java programming language: they were strongly resisted for many years. Research the controversy surrounding them. Where do your sympathies lie? What alternative proposals were rejected? Do you find any of them appealing?
3.42 举三个例子,说明你熟悉的语言没有提供哪些功能,但在其他语言中却很常见。你认为缺少这些功能的原因是什么?它们会使语言的实现复杂化吗?如果是这样,这种复杂化(你认为)是否合理?
3.42 Give three examples of features that are not provided in some language with which you are familiar, but that are common in other languages. Why do you think these features are missing? Would they complicate the implementation of the language? If so, would the complication (in your judgment) be justified?
3.43–3.47 更深入。
3.43–3.47 In More Depth.
本章追溯了多种语言中命名和作用域机制的演变,包括 Fortran(多个版本)、Basic、Algol 60 和 68、Pascal、Simula、C 和 C++、Euclid、Turing、Modula(1、2 和 3)、Ada(83 和 95)、Oberon、Eiffel、Perl、Tcl、Python、Ruby、Rust、Java 和 C#。所有这些书目的参考资料都可以在附录 A中找到。
This chapter has traced the evolution of naming and scoping mechanisms through a very large number of languages, including Fortran (several versions), Basic, Algol 60 and 68, Pascal, Simula, C and C++, Euclid, Turing, Modula (1, 2, and 3), Ada (83 and 95), Oberon, Eiffel, Perl, Tcl, Python, Ruby, Rust, Java, and C#. Bibliographic references for all of these can be found in Appendix A.
模块和对象都起源于 Simula,它是由 Dahl、Nygaard、Myhrhaug 等人在 20 世纪 60 年代中期在挪威计算中心开发的。(Simula I 于 1964 年实现;本书中的描述涉及 Simula 67。)Clu、Modula、Euclid 和相关语言的开发人员在 20 世纪 70 年代改进了 Simula 的封装机制。Simula 的其他创新(尤其是继承和动态方法绑定)为 Smalltalk 提供了灵感,Smalltalk 是面向对象语言的最初版本,可以说是最纯粹的。现代面向对象语言(包括 Eiffel、C++、Java、C#、Python 和 Ruby)在很大程度上代表了封装与继承和动态方法绑定演进路线的重新整合。
Both modules and objects trace their roots to Simula, which was developed by Dahl, Nygaard, Myhrhaug, and others at the Norwegian Computing Center in the mid-1960s. (Simula I was implemented in 1964; descriptions in this book pertain to Simula 67.) The encapsulation mechanisms of Simula were refined in the 1970s by the developers of Clu, Modula, Euclid, and related languages. Other Simula innovations—inheritance and dynamic method binding in particular—provided the inspiration for Smalltalk, the original and arguably purest of the object-oriented languages. Modern object-oriented languages, including Eiffel, C++, Java, C#, Python, and Ruby, represent to a large extent a reintegration of the evolutionary lines of encapsulation on the one hand and inheritance and dynamic method binding on the other.
信息隐藏的概念起源于 Parnas 的经典论文“关于将系统分解为模块所用的标准” [ Par72 ]。关于命名、作用域和抽象机制的比较讨论可以在以下地方找到:Liskov 等人对 Clu 的讨论 [ LSAS77 ],Liskov 和 Guttag 的文本 [ LG86,第 4 章],Ada 基本原理 [ IBFW91,第 9 – 12章],Harbison 关于 Modula-3 的文本 [ Har92,第 8 – 9章],Wirth 早期关于模块的工作 [ Wir80 ],以及他后来对 Modula 和 Oberon 的讨论 [ Wir88a,Wir07 ]。关于面向对象语言的更多信息,请参见第 10 章。
The notion of information hiding originates in Parnas's classic paper, “On the Criteria to be Used in Decomposing Systems into Modules” [Par72]. Comparative discussions of naming, scoping, and abstraction mechanisms can be found, among other places, in Liskov et al.'s discussion of Clu [LSAS77], Liskov and Guttag's text [LG86, Chap. 4], the Ada Rationale [IBFW91, Chaps. 9–12], Harbison's text on Modula-3 [Har92, Chaps. 8–9], Wirth's early work on modules [Wir80], and his later discussion of Modula and Oberon [Wir88a, Wir07]. Further information on object-oriented languages can be found in Chapter 10.
有关重载和多态性的详细讨论,请参阅 Cardelli 和 Wegner 的综述 [ CW85 ]。Cailliau [ Cai82 ] 对第 3.3.3 节中指出的许多范围界定陷阱进行了轻松的讨论。Abelson 和 Sussman [ AS96,第 11n 页] 将“语法糖”一词归功于 Peter Landin。
For a detailed discussion of overloading and polymorphism, see the survey by Cardelli and Wegner [CW85]. Cailliau [Cai82] provides a lighthearted discussion of many of the scoping pitfalls noted in Section 3.3.3. Abelson and Sussman [AS96, p. 11n] attribute the term “syntactic sugar” to Peter Landin.
Jarvi 和 Freeman 的论文 [ JF10 ] 描述了 C++ 的 Lambda 表达式。Java 的 Lambda 表达式是在 Java Community Process 的 JSR 335 下开发的(文档位于jcp.org)。
Lambda expressions for C++ are described in the paper of Jarvi and Freeman [JF10]. Lambda expressions for Java were developed under JSR 335 of the Java Community Process (documentation at jcp.org).
在 第 2 章中 我们讨论了编程语言语法这一主题。在本章中,我们将讨论语义这一主题。非正式地说,语法涉及有效程序的形式,而语义涉及其含义。含义之所以重要,至少有两个原因:它使我们能够执行超越单纯形式的规则(例如,类型一致性),并且它提供了我们生成等效输出程序所需的信息。
In Chapter 2 we considered the topic of programming language syntax. In the current chapter we turn to the topic of semantics. Informally, syntax concerns the form of a valid program, while semantics concerns its meaning. Meaning is important for at least two reasons: it allows us to enforce rules (e.g., type consistency) that go beyond mere form, and it provides the information we need in order to generate an equivalent output program.
通常来说,语言的语法就是语言定义中可以方便地用上下文无关文法描述的部分,而语义则是定义中不能方便地描述的部分。这种惯例在实践中很有用,尽管它并不总是与直觉一致。例如,当我们要求子程序调用中包含的参数数量与子程序定义中的形式参数数量相匹配时,很容易说这个要求是语法问题。毕竟,我们可以计算参数而不知道它们的含义。不幸的是,我们无法使用上下文无关规则来计算它们。同样,虽然可以编写一个上下文无关文法,其中每个函数都必须包含至少一个返回语句,但所需的复杂性使这种策略非常没有吸引力。一般来说,任何要求编译器比较相距很远的事物或计算未正确嵌套的事物的规则最终都是语义问题。
It is conventional to say that the syntax of a language is precisely that portion of the language definition that can be described conveniently by a context-free grammar, while the semantics is that portion of the definition that cannot. This convention is useful in practice, though it does not always agree with intuition. When we require, for example, that the number of arguments contained in a call to a subroutine match the number of formal parameters in the subroutine definition, it is tempting to say that this requirement is a matter of syntax. After all, we can count arguments without knowing what they mean. Unfortunately, we cannot count them with context-free rules. Similarly, while it is possible to write a context-free grammar in which every function must contain at least one return statement, the required complexity makes this strategy very unattractive. In general, any rule that requires the compiler to compare things that are separated by long distances, or to count things that are not properly nested, ends up being a matter of semantics.
语义规则进一步分为静态语义和动态语义,尽管两者之间的界限仍然有些模糊。编译器在编译时强制执行静态语义规则。它生成代码以在运行时强制执行动态语义规则(或调用执行此操作的库例程)。某些错误(例如除以零或尝试使用越界下标索引数组)通常无法在编译时捕获,因为它们可能仅针对某些输入值或任意复杂代码的某些行为发生。在特殊情况下,编译器可能能够判断某个错误将始终发生或永远不会发生,而与运行时输入无关。在这些情况下,编译器可以在编译时生成错误消息,或者根据需要避免生成代码以在运行时执行检查。然而,可计算性理论的基本结果告诉我们,没有算法可以对任意程序正确做出这些预测:不可避免地会出现这样的情况:错误总是会发生,但编译器无法分辨,必须将错误消息延迟到运行时;也会出现这样的情况:错误永远不会发生,但编译器无法分辨,必须承担不必要的运行时检查的成本。
Semantic rules are further divided into static and dynamic semantics, though again the line between the two is somewhat fuzzy. The compiler enforces static semantic rules at compile time. It generates code to enforce dynamic semantic rules at run time (or to call library routines that do so). Certain errors, such as division by zero, or attempting to index into an array with an out-of-bounds subscript, cannot in general be caught at compile time, since they may occur only for certain input values, or certain behaviors of arbitrarily complex code. In special cases, a compiler maybe able to tell that a certain error will always or never occur, regardless of run-time input. In these cases, the compiler can generate an error message at compile time, or refrain from generating code to perform the check at run time, as appropriate. Basic results from computability theory, however, tell us that no algorithm can make these predictions correctly for arbitrary programs: there will inevitably be cases in which an error will always occur, but the compiler cannot tell, and must delay the error message until run time; there will also be cases in which an error can never occur, but the compiler cannot tell, and must incur the cost of unnecessary run-time checks.
语义分析和中间代码生成都可以用注释或解析树或语法树的修饰来描述。注释本身称为属性。后续章节将出现大量静态和动态语义规则的示例。在本章中,我们主要关注编译器用来执行静态规则的机制。我们将在第 15 章中讨论中间代码生成(包括生成用于动态语义检查的代码)。
Both semantic analysis and intermediate code generation can be described in terms of annotation, or decoration of a parse tree or syntax tree. The annotations themselves are known as attributes. Numerous examples of static and dynamic semantic rules will appear in subsequent chapters. In this current chapter we focus primarily on the mechanisms a compiler uses to enforce the static rules. We will consider intermediate code generation (including the generation of code for dynamic semantic checks) in Chapter 15.
在第 4.1 节中,我们将更详细地考虑语义分析器的作用,考虑它需要执行的规则以及它与其他编译阶段的关系。本章的其余部分将专门讨论属性语法这一主题。属性语法为树的修饰提供了一个正式的框架。即使在那些不将解析树或语法树构建为显式数据结构的编译器中,这个框架也是一个有用的概念工具。我们在第 4.2 节中介绍了属性语法的概念。然后,我们考虑了在实践中应用此类语法的各种方式。第 4.3 节讨论了属性流的问题,它限制了树节点的修饰顺序。在实践中,大多数编译器要求在 LL 或 LR 解析过程中修饰解析树(或评估解析树中存在的属性(如果有))。第 4.4 节介绍了动作例程,作为此类“即时”评估的临时机制。在第 4.5 节(主要在配套网站上)中,我们考虑了解析树属性的空间管理。
In Section 4.1 we consider the role of the semantic analyzer in more detail, considering both the rules it needs to enforce and its relationship to other phases of compilation. Most of the rest of the chapter is then devoted to the subject of attribute grammars. Attribute grammars provide a formal framework for the decoration of a tree. This framework is a useful conceptual tool even in compilers that do not build a parse tree or syntax tree as an explicit data structure. We introduce the notion of an attribute grammar in Section 4.2. We then consider various ways in which such grammars can be applied in practice. Section 4.3 discusses the issue of attribute flow, which constrains the order(s) in which nodes of a tree can be decorated. In practice, most compilers require decoration of the parse tree (or the evaluation of attributes that would reside in a parse tree if there were one) to occur in the process of an LL or LR parse. Section 4.4 presents action routines as an ad hoc mechanism for such “on-the-fly” evaluation. In Section 4.5 (mostly on the companion site) we consider the management of space for parse tree attributes.
由于解析树必须反映 CFG 的结构,因此解析树往往非常复杂(回想一下图 1.5中的示例)。解析完成后,我们通常希望用更直接地反映输入程序的语法树替换解析树(图 1.6 )。一个特别常见的编译器组织在解析过程中使用动作例程,其目的仅仅是构建语法树。然后在单独的遍历过程中修饰语法树,如果需要,可以使用单独的属性语法将其形式化。我们将在第 4.6 节中考虑语法树的修饰。
Because they have to reflect the structure of the CFG, parse trees tend to be very complicated (recall the example in Figure 1.5). Once parsing is complete, we typically want to replace the parse tree with a syntax tree that reflects the input program in a more straightforward way (Figure 1.6). One particularly common compiler organization uses action routines during parsing solely for the purpose of constructing the syntax tree. The syntax tree is then decorated during a separate traversal, which can be formalized, if desired, with a separate attribute grammar. We consider the decoration of syntax trees in Section 4.6.
编程语言在语义规则的选择上差异巨大。例如,Lisp 方言允许对任意数字类型进行“混合模式”运算,它们将根据需要自动将其从整数提升为有理数,再提升为浮点数或“大数”(扩展)精度,以保持精度。而 Ada 则按照惯例为每个数字变量分配一个特定类型,并要求程序员在表达式中组合它们时明确地在这些变量之间进行转换。不同语言在要求其实现执行动态检查的程度上也有所不同。在一个极端,C 根本不需要检查,除了硬件附带的那些“免费”检查(例如,除以零,或试图访问程序边界之外的内存)。在另一个极端,Java 不遗余力地检查尽可能多的规则,部分原因是为了确保不受信任的程序不会做任何事情来损坏它所运行的机器的内存或文件。语义分析器的作用是强制执行所有静态语义规则并使用中间代码生成器所需的信息注释程序。这些信息包括澄清(这是浮点加法,而不是整数;这是对全局变量x的引用)和动态语义检查的要求。
Programming languages vary dramatically in their choice of semantic rules. Lisp dialects, for example, allow “mixed-mode” arithmetic on arbitrary numeric types, which they will automatically promote from integer to rational to floating-point or “bignum” (extended) precision, as required to maintain precision. Ada, by contract, assigns a specific type to every numeric variable, and requires the programmer to convert among these explicitly when combining them in expressions. Languages also vary in the extent to which they require their implementations to perform dynamic checks. At one extreme, C requires no checks at all, beyond those that come “free” with the hardware (e.g., division by zero, or attempted access to memory outside the bounds of the program). At the other extreme, Java takes great pains to check as many rules as possible, in part to ensure that an untrusted program cannot do anything to damage the memory or files of the machine on which it runs. The role of the semantic analyzer is to enforce all static semantic rules and to annotate the program with information needed by the intermediate code generator. This information includes both clarifications (this is floating-point addition, not integer; this is a reference to the global variable x) and requirements for dynamic semantic checks.
在典型的编译器中,分析和中间代码生成标志着前端计算的结束。然而,前端和后端之间的确切分工可能因编译器而异:很难确切地说分析(弄清楚程序的含义)在哪里结束,综合(以某种新形式表达该含义)在哪里开始(并且如第1.6 节所述,两者之间可能存在一个“中间端”)。许多编译器还使程序经历多种中间形式。在一种常见的组织中(第15 章将更详细地描述),语义分析器创建带注释的语法树,然后中间代码生成器将其转换为线性形式,让人联想到某些理想机器的汇编语言。在独立于机器的代码改进之后,这种线性形式被转换成另一种形式,更紧密地模仿目标机器的汇编语言。该形式可能会进行特定于机器的代码改进。
In the typical compiler, analysis and intermediate code generation mark the end of front end computation. The exact division of labor between the front end and the back end, however, may vary from compiler to compiler: it can be hard to say exactly where analysis (figuring out what the program means) ends and synthesis (expressing that meaning in some new form) begins (and as noted in Section 1.6 there maybe a “middle end” in between). Many compilers also carry a program through more than one intermediate form. In one common organization, described in more detail in Chapter 15, the semantic analyzer creates an annotated syntax tree, which the intermediate code generator then translates into a linear form reminiscent of the assembly language for some idealized machine. After machine-independent code improvement, this linear form is then translated into yet another form, patterned more closely on the assembly language of the target machine. That form may undergo machine-specific code improvement.
编译器在语义分析和中间代码生成与解析交错的程度上也有所不同。在完全分离的阶段中,解析器将完整的解析树传递给语义分析器,语义分析器将其转换为语法树,填充符号表,执行语义检查,并将其传递给代码生成器。在完全交错的阶段中,可能不需要构建整个解析树或语法树:解析器可以在解析源代码的每个表达式、语句或子例程时动态调用语义检查和代码生成例程。我们将重点介绍一种组织,其中语法树的构建与解析交错(并且不构建解析树),但语义分析发生在单独的语法树遍历期间。
Compilers also vary in the extent to which semantic analysis and intermediate code generation are interleaved with parsing. With fully separated phases, the parser passes a full parse tree on to the semantic analyzer, which converts it to a syntax tree, fills in the symbol table, performs semantic checks, and passes it on to the code generator. With fully interleaved phases, there may be no need to build either the parse tree or the syntax tree in its entirety: the parser can call semantic check and code generation routines on the fly as it parses each expression, statement, or subroutine of the source. We will focus on an organization in which construction of the syntax tree is interleaved with parsing (and the parse tree is not built), but semantic analysis occurs during a separate traversal of the syntax tree.
许多生成动态检查代码的编译器都提供了禁用动态检查的选项(如果需要)。一些组织习惯在程序开发和测试期间启用动态检查,然后在生产使用时禁用它们,以提高执行速度。这种做法的合理性值得怀疑:编程语言设计领域的关键人物之一 Tony Hoare1曾将禁用语义检查的程序员比作帆船爱好者,他在陆地上训练时穿着救生衣,但出海时却脱掉它 [ Hoa89,第 198 页]。在生产使用中出错的可能性可能比在测试中要小,但未检测到的错误的后果要严重得多。此外,在现代处理器上,动态检查通常可以在原本未使用的流水线槽中执行,从而使这些槽几乎不花费任何成本。另一方面,一些动态检查(例如,确保 C 语言中的指针算法保持在数组范围内)非常昂贵,因此很少实现。
Many compilers that generate code for dynamic checks provide the option of disabling them if desired. It is customary in some organizations to enable dynamic checks during program development and testing, and then disable them for production use, to increase execution speed. The wisdom of this practice is questionable: Tony Hoare, one of the key figures in programming language design,1 has likened the programmer who disables semantic checks to a sailing enthusiast who wears a life jacket when training on dry land, but removes it when going to sea [Hoa89, p. 198]. Errors may be less likely in production use than they are in testing, but the consequences of an undetected error are significantly worse. Moreover, on modern processors it is often possible for dynamic checks to execute in pipeline slots that would otherwise go unused, making them virtually free. On the other hand, some dynamic checks (e.g., ensuring that pointer arithmetic in C remains within the bounds of an array) are sufficiently expensive that they are rarely implemented.
某些语言(例如 Euclid、Eiffel 和 Ada 2012)还明确支持不变量、先决条件和后置条件。这些本质上是结构化断言。不变量在给定代码体的所有“干净点”上都应该为真。在 Eiffel 中,程序员可以在类内的数据上指定一个不变量:该不变量将在该类的每个方法(子程序)的开始和结束时自动进行检查。循环的类似不变量在每次迭代之前和之后都应该为真。先决条件和后置条件应该分别在子程序的开始和结束时为真。在 Euclid 中,在子程序头部指定一次的后置条件不仅会在子程序文本的末尾进行检查,还会在每个返回语句处进行检查。
Some languages (e.g., Euclid, Eiffel, and Ada 2012) also provide explicit support for invariants, preconditions, and postconditions. These are essentially structured assertions. An invariant is expected to be true at all “clean points” of a given body of code. In Eiffel, the programmer can specify an invariant on the data inside a class: the invariant will be checked, automatically, at the beginning and end of each of the class's methods (subroutines). Similar invariants for loops are expected to be true before and after every iteration. Pre- and postconditions are expected to be true at the beginning and end of subroutines, respectively. In Euclid, a postcondition, specified once in the header of a subroutine, will be checked not only at the end of the subroutine's text, but at every return statement as well.
当然,断言可用于覆盖其他三种检查,但不那么清晰或简洁。不变量、先决条件和后置条件是它们适用的代码头中的重要部分,并且可以覆盖大量原本需要断言的地方。Euclid 和 Eiffel 实现允许程序员在需要时禁用断言和相关构造,以消除它们的运行时成本。
Assertions, of course, could be used to cover the other three sorts of checks, but not as clearly or succinctly. Invariants, preconditions, and postconditions are a prominent part of the header of the code to which they apply, and can cover a potentially large number of places where an assertion would otherwise be required. Euclid and Eiffel implementations allow the programmer to disable assertions and related constructs when desired, to eliminate their run-time cost.
一般而言,预测运行时行为的编译时算法称为静态分析。如果这种分析允许编译器确定给定程序是否始终遵循规则,则称其为精确分析。例如,在 Ada 和 ML 等语言中,类型检查是静态且精确的:编译器确保在运行时不会以不适合其类型的方式使用变量。相比之下,Lisp、Smalltalk、Python 和 Ruby 等语言通过接受动态类型检查的运行时开销,获得了更大的灵活性,同时保持了完全类型安全。(我们将在第 7 章中更详细地介绍类型检查。)
In general, compile-time algorithms that predict run-time behavior are known as static analysis. Such analysis is said to be precise if it allows the compiler to determine whether a given program will always follow the rules. Type checking, for example, is static and precise in languages like Ada and ML: the compiler ensures that no variable will ever be used at run time in a way that is inappropriate for its type. By contrast, languages like Lisp, Smalltalk, Python, and Ruby obtain greater flexibility, while remaining completely type-safe, by accepting the runtime overhead of dynamic type checks. (We will cover type checking in more detail in Chapter 7.)
静态分析在不精确的情况下也很有用。编译器通常会在编译时检查它们能检查的内容,然后生成代码来动态检查其余部分。例如,在 Java 中,类型检查主要是静态的,但动态加载的类和类型转换可能需要运行时检查。同样,许多编译器执行广泛的静态分析,试图消除对数组下标、变体记录标签或潜在悬垂指针(将在第 8 章中讨论)进行动态检查的需要。
Static analysis can also be useful when it isn't precise. Compilers will often check what they can at compile time and then generate code to check the rest dynamically. In Java, for example, type checking is mostly static, but dynamically loaded classes and type casts may require run-time checks. In a similar vein, many compilers perform extensive static analysis in an attempt to eliminate the need for dynamic checks on array subscripts, variant record tags, or potentially dangling pointers (to be discussed in Chapter 8).
如果我们将省略不必要的动态检查视为一种性能优化,那么寻找静态分析可以改进代码的其他方式也是很自然的。我们将在第17 章中更详细地讨论这个主题。例子包括别名分析,它决定何时可以将值安全地缓存在寄存器中、以“无序”计算或由并发线程访问;逃逸分析,它决定何时将对值的所有引用限制在给定的上下文中,从而允许在堆栈而不是堆上分配值,或者无需锁即可访问;以及子类型分析,它决定何时保证面向对象语言中的变量具有某种子类型,以便可以调用其方法而无需动态分派。
If we think of the omission of unnecessary dynamic checks as a performance optimization, it is natural to look for other ways in which static analysis may enable code improvement. We will consider this topic in more detail in Chapter 17. Examples include alias analysis, which determines when values can be safely cached in registers, computed “out of order,” or accessed by concurrent threads; escape analysis, which determines when all references to a value will be confined to a given context, allowing the value to be allocated on the stack instead of the heap, or to be accessed without locks; and subtype analysis, which determines when a variable in an object-oriented language is guaranteed to have a certain subtype, so that its methods can be called without dynamic dispatch.
如果某种优化可能导致某些程序的代码不正确,则称其为不安全的优化。如果它通常可以提高性能,但在某些情况下会降低性能,则称其为推测性的。如果编译器只有在能够保证优化既安全又有效时才应用优化,则称其为保守性的。相反,乐观的编译器可能会大量使用推测性优化。它还可以通过生成两个版本的代码来进行不安全的优化,并根据编译时不可用的信息进行动态检查以在两个版本之间进行选择。推测性优化的例子包括非绑定预取(它试图在需要数据之前将其放入缓存中)和跟踪调度(它重新排列代码以期提高处理器管道和指令缓存的性能)。
An optimization is said to be unsafe if it may lead to incorrect code in certain programs. It is said to be speculative if it usually improves performance, but may degrade it in certain cases. A compiler is said to be conservative if it applies optimizations only when it can guarantee that they will be both safe and effective. By contrast, an optimistic compiler may make liberal use of speculative optimizations. It may also pursue unsafe optimizations by generating two versions of the code, with a dynamic check that chooses between them based on information not available at compile time. Examples of speculative optimization include nonbinding prefetches, which try to bring data into the cache before they are needed, and trace scheduling, which rearranges code in hopes of improving the performance of the processor pipeline and the instruction cache.
为了消除动态检查,语言设计者可能会选择收紧语义规则,禁止保守分析失败的程序。例如,ML 类型系统(第 7.2.4 节)避免了 Lisp 的动态类型检查,但不允许 Lisp 支持的某些有用的编程习惯用法。类似地,Java 和 C# 的明确赋值规则(第 6.1.3 节)允许编译器确保变量在表达式中使用之前始终被赋值,但不允许某些在 C 中合法(且正确)的程序。
To eliminate dynamic checks, language designers may choose to tighten semantic rules, banning programs for which conservative analysis fails. The ML type system, for example (Section 7.2.4), avoids the dynamic type checks of Lisp, but disallows certain useful programming idioms that Lisp supports. Similarly, the definite assignment rules of Java and C# (Section 6.1.3) allow the compiler to ensure that a variable is always given a value before it is used in an expression, but disallow certain programs that are legal (and correct) in C.
语义函数的符号(无论是内联的还是显式的)和属性本身的类型都不是属性语法概念所固有的。语法的目的只是将含义与解析树或语法树的节点关联起来。为此,我们可以使用任何含义已经明确定义的符号和类型。在示例 4.4和4.5中,我们使用从普通算术中提取的语义函数将数值与 CFG 中的符号关联起来,从而与解析树节点关联起来。在完整编程语言的编译器或解释器中,树节点的属性可能包括
Neither the notation for semantic functions (whether in-line or explicit) nor the types of the attributes themselves is intrinsic to the notion of an attribute grammar. The purpose of the grammar is simply to associate meaning with the nodes of a parse tree or syntax tree. Toward that end, we can use any notation and types whose meanings are already well defined. In Examples 4.4 and 4.5, we associated numeric values with the symbols in a CFG—and thus with parse tree nodes—using semantic functions drawn from ordinary arithmetic. In a compiler or interpreter for a full programming language, the attributes of tree nodes might include
■ for an identifier, a reference to information about it in the symbol table
■ 对于表达式,其类型
■ for an expression, its type
■ 对于语句或表达式,引用编译器的中间形式中相应的代码
■ for a statement or expression, a reference to corresponding code in the compiler's intermediate form
■ 对于几乎任何构造,都会指示相应源代码开始的文件名、行和列
■ for almost any construct, an indication of the file name, line, and column where the corresponding source code begins
■ 对于任何内部节点,在下面的子树中发现的语义错误列表
■ for any internal node, a list of semantic errors found in the subtree below
对于除翻译之外的其他目的(例如,在定理证明器或独立于机器的语言定义中),属性可能来自指称语义、操作语义或公理语义的学科。感兴趣的读者可以在本章末尾的书目注释中找到参考资料。
For purposes other than translation—e.g., in a theorem prover or machine-independent language definition—attributes might be drawn from the disciplines of denotational, operational, or axiomatic semantics. Interested readers can find references in the Bibliographic Notes at the end of the chapter.
图 4.1中的属性语法非常简单。每个符号最多有一个属性(标点符号没有)。此外,它们都是所谓的合成属性:它们的值仅在其符号出现在左侧的产生式中计算(合成)。对于像图 4.2中的带注释的解析树,这意味着属性流(信息从一个节点移动到另一个节点的模式)完全是自下而上的。
The attribute grammar of Figure 4.1 is very simple. Each symbol has at most one attribute (the punctuation marks have none). Moreover, they are all so-called synthesized attributes: their values are calculated (synthesized) only in productions in which their symbol appears on the left-hand side. For annotated parse trees like the one in Figure 4.2, this means that the attribute flow—the pattern in which information moves from node to node—is entirely bottom-up.
所有属性都经过合成的属性语法被称为S 属性语法。S 属性语法中语义函数的参数始终是当前产生式右侧符号的属性,返回值始终放在产生式左侧的属性中。标记(终端)通常具有内在属性(例如,标识符的字符串表示或数字常量的值);在编译器中,这些是扫描器初始化的合成属性。
An attribute grammar in which all attributes are synthesized is said to be S-attributed. The arguments to semantic functions in an S-attributed grammar are always attributes of symbols on the right-hand side of the current production, and the return value is always placed into an attribute of the left-hand side of the production. Tokens (terminals) often have intrinsic properties (e.g., the character-string representation of an identifier or the value of a numeric constant); in a compiler these are synthesized attributes initialized by the scanner.
一般来说,我们可以想象(并且实际上也需要)这样的属性,当它们的符号位于当前产生式的右侧时,它们的值就会被计算出来。这样的属性被称为继承的。它们允许上下文信息从上方或侧面流入符号,这样,该产生式的规则就可以根据周围的上下文以不同的方式执行(或生成不同的值)。符号表信息通常通过继承的属性在符号之间传递。解析树根的继承属性也可用于表示外部环境(目标机器的特性、编译器的命令行参数等)。
In general, we can imagine (and will in fact have need of) attributes whose values are calculated when their symbol is on the right-hand side of the current production. Such attributes are said to be inherited. They allow contextual information to flow into a symbol from above or from the side, so that the rules of that production can be enforced in different ways (or generate different values) depending on surrounding context. Symbol table information is commonly passed from symbol to symbol by means of inherited attributes. Inherited attributes of the root of the parse tree can also be used to represent the external environment (characteristics of the target machine, command-line arguments to the compiler, etc.).
如果我们想创建一个属性语法,将整个表达式的值累积到树的根中,我们会遇到一个问题:因为减法是左结合的,所以我们不能用一个数值来总结根的右子树。如果我们想用 S 属性语法自下而上地装饰树,我们必须准备在最顶层expr_tail节点的属性中描述任意数量的右操作数(参见练习 4.4)。这确实是可能的,但它违背了形式主义的目的:实际上,它要求我们将整个树嵌入到单个节点的属性中,并在单个语义函数中完成所有实际工作。
If we want to create an attribute grammar that accumulates the value of the overall expression into the root of the tree, we have a problem: because subtraction is left associative, we cannot summarize the right subtree of the root with a single numeric value. If we want to decorate the tree bottom-up, with an S-attributed grammar, we must be prepared to describe an arbitrary number of right operands in the attributes of the top-most expr_tail node (see Exercise 4.4). This is indeed possible, but it defeats the purpose of the formalism: in effect, it requires us to embed the entire tree into the attributes of a single node, and do all the real work inside a single semantic function.
正如上下文无关语法没有指定应如何解析它一样,属性语法也没有指定应以何种顺序调用属性规则。换句话说,这两种表示法都是声明性的:它们定义了一组有效树,但没有说明如何构建或修饰它们。除此之外,这意味着对于给定的产生式,属性规则的列出顺序并不重要;属性流可能要求它们以任何顺序执行。如果在图 4.3中,我们要反转规则在产生式 1、2、3、5、6 和/或 7 中出现的顺序(首先列出 symbol.val 的规则),这将是一个纯粹表面上的改变;语法不会改变。
Just as a context-free grammar does not specify how it should be parsed, an attribute grammar does not specify the order in which attribute rules should be invoked. Put another way, both notations are declarative: they define a set of valid trees, but they don't say how to build or decorate them. Among other things, this means that the order in which attribute rules are listed for a given production is immaterial; attribute flow may require them to execute in any order. If, in Figure 4.3, we were to reverse the order in which the rules appear in productions 1, 2, 3, 5, 6, and/or 7 (listing the rule for symbol.val first), it would be a purely cosmetic change; the grammar would not be altered.
如果属性语法的规则为每棵可能的解析树的属性确定了一组唯一的值,我们就说该属性语法是定义良好的。如果属性语法永远不会导致解析树中的属性流图中存在循环,即如果任何解析树中的属性都永远不会(传递地)依赖于自身,则该属性语法是非循环的。(如果属性保证收敛到一个唯一的值,那么语法可以是循环的,并且仍然定义良好。)一般来说,实际的属性语法往往是非循环的。
We say an attribute grammar is well defined if its rules determine a unique set of values for the attributes of every possible parse tree. An attribute grammar is noncircular if it never leads to a parse tree in which there are cycles in the attribute flow graph—that is, if no attribute, in any parse tree, ever depends (transitively) on itself. (A grammar can be circular and still be well defined if attributes are guaranteed to converge to a unique value.) As a general rule, practical attribute grammars tend to be noncircular.
通过按照与树的属性流一致的顺序调用属性语法规则来修饰解析树的算法称为翻译方案。也许最简单的方案是反复遍历树,调用任何参数都已定义的语义函数,并在完成一次遍历且其中没有值发生变化时停止。这种方案被称为无知的,因为它不利用解析树或语法的特殊知识。只有语法定义明确时它才会停止。通过动态方案可以根据给定解析树的结构调整求值顺序,可以实现更好的性能,至少对于非循环语法而言是这样——例如,通过构建属性流图的拓扑排序,然后按照与排序一致的顺序调用规则。
An algorithm that decorates parse trees by invoking the rules of an attribute grammar in an order consistent with the tree's attribute flow is called a translation scheme. Perhaps the simplest scheme is one that makes repeated passes over a tree, invoking any semantic function whose arguments have all been defined, and stopping when it completes a pass in which no values change. Such a scheme is said to be oblivious, in the sense that it exploits no special knowledge of either the parse tree or the grammar. It will halt only if the grammar is well defined. Better performance, at least for noncircular grammars, may be achieved by a dynamic scheme that tailors the evaluation order to the structure of a given parse tree—for example, by constructing a topological sort of the attribute flow graph and then invoking rules in an order consistent with the sort.
然而,最快的翻译方案往往是静态的——基于对属性语法本身结构的分析,然后机械地应用于由语法产生的任何树。与 LL 和 LR 解析器一样,线性时间静态翻译方案只能为某些受限的语法类别设计。S 属性语法(如图 4.1所示)是此类中最简单的一种。由于 S 属性语法中的属性流严格自下而上,因此可以通过访问解析树的节点来评估属性,访问顺序与 LR 系列解析器生成这些节点的顺序完全相同。事实上,可以在自下而上的解析过程中动态评估属性,从而交错解析和语义分析(属性评估)。
The fastest translation schemes, however, tend to be static—based on an analysis of the structure of the attribute grammar itself, and then applied mechanically to any tree arising from the grammar. Like LL and LR parsers, linear-time static translation schemes can be devised only for certain restricted classes of grammars. S-attributed grammars, such as the one in Figure 4.1, form the simplest such class. Because attribute flow in an S-attributed grammar is strictly bottom-up, attributes can be evaluated by visiting the nodes of the parse tree in exactly the same order that those nodes are generated by an LR-family parser. In fact, the attributes can be evaluated on the fly during a bottom-up parse, thereby interleaving parsing and semantic analysis (attribute evaluation).
图 4.3的属性语法比图 4.1的属性语法稍微混乱一些,但是它仍然是L 属性的:可以通过从左到右、深度优先遍历访问解析树的节点来评估其属性(与自上而下的解析过程中访问它们的顺序相同 - 参见图 4.4)。如果我们说属性As 依赖于属性Bt (如果Bt曾被传递给一个返回As值的语义函数),那么我们可以使用以下两个规则更正式地定义 L 属性语法:(1)左侧符号的每个合成属性仅依赖于该符号自己的继承属性或产生式右侧符号的属性(合成或继承);(2)右侧符号的每个继承属性仅依赖于左侧符号的继承属性或右侧其左侧符号的属性(合成或继承)。
The attribute grammar of Figure 4.3 is a good bit messier than that of Figure 4.1, but it is still L-attributed: its attributes can be evaluated by visiting the nodes of the parse tree in a single left-to-right, depth-first traversal (the same order in which they are visited during a top-down parse—see Figure 4.4). If we say that an attribute A.s depends on an attribute B.t if B.t is ever passed to a semantic function that returns a value for A.s, then we can define L-attributed grammars more formally with the following two rules: (1) each synthesized attribute of a left-hand-side symbol depends only on that symbol's own inherited attributes or on attributes (synthesized or inherited) of the production's right-hand-side symbols, and (2) each inherited attribute of a right-hand-side symbol depends only on inherited attributes of the left-hand-side symbol or on attributes (synthesized or inherited) of symbols to its left in the right-hand side.
因为 L 属性语法允许使用右侧符号的属性初始化产生式左侧属性的规则,所以每个 S 属性语法也是 L 属性语法。反之则不然:S 属性语法不允许初始化右侧的属性,因此存在非 S 属性的 L 属性语法。
Because L-attributed grammars permit rules that initialize attributes of the left-hand side of a production using attributes of symbols on the right-hand side, every S-attributed grammar is also an L-attributed grammar. The reverse is not the case: S-attributed grammars do not permit the initialization of attributes on the right-hand side, so there are L-attributed grammars that are not S-attributed.
S 属性属性语法是属性语法中最通用的一类,可以在 LR 解析期间动态执行求值。L 属性语法是属性语法中最通用的一类,可以在 LL 解析期间动态执行求值。如果我们将语义分析(以及可能的中间代码生成)与解析交错,则自下而上的解析器通常必须与 S 属性翻译方案配对;自上而下的解析器必须与 L 属性翻译方案配对。(根据语法的结构,自下而上的解析器通常可以容纳一些非 S 属性的属性流;我们将在 C-4.5.1 节中考虑这种可能性。)如果我们选择将解析和语义分析分成单独的过程,那么构建解析树或语法树的代码仍必须使用 S 属性或 L 属性翻译方案(视情况而定),但语义分析器可以根据需要使用更强大的方案。某些任务最容易通过非 L 属性方案来完成,例如生成“短路”布尔表达式的代码(将在第 6.1.5和6.4.1节中讨论)。
S-attributed attribute grammars are the most general class of attribute grammars for which evaluation can be implemented on the fly during an LR parse. L-attributed grammars are the most general class for which evaluation can be implemented on the fly during an LL parse. If we interleave semantic analysis (and possibly intermediate code generation) with parsing, then a bottom-up parser must in general be paired with an S-attributed translation scheme; a top-down parser must be paired with an L-attributed translation scheme. (Depending on the structure of the grammar, it is often possible for a bottom-up parser to accommodate some non-S-attributed attribute flow; we consider this possibility in Section C-4.5.1.) If we choose to separate parsing and semantic analysis into separate passes, then the code that builds the parse tree or syntax tree must still use an S-attributed or L-attributed translation scheme (as appropriate), but the semantic analyzer can use a more powerful scheme if desired. There are certain tasks, such as the generation of code for “short-circuit” Boolean expressions (to be discussed in Sections 6.1.5 and 6.4.1), that are easiest to accomplish with a non-L-attributed scheme.
将语义分析和代码生成与解析交错进行的编译器被称为一次性编译器。3目前尚不清楚将语义分析与解析交错进行的编译器是使编译器更简单还是更复杂;这主要取决于个人喜好。如果中间代码生成与解析交错进行,则根本不需要构建语法树(当然,除非语法树是中间代码)。此外,通常可以动态地将中间代码写入输出文件,而不是将其累积在解析树根的属性中。由此节省的空间对于主存储器非常小的上一代计算机来说非常重要。另一方面,在单独遍历语法树时更容易执行语义分析,因为该树比解析树更好地反映了程序的语义结构,尤其是使用自上而下的解析器时,并且因为可以选择以解析器选择的顺序以外的顺序遍历树。
A compiler that interleaves semantic analysis and code generation with parsing is said to be a one-pass compiler.3 It is unclear whether interleaving semantic analysis with parsing makes a compiler simpler or more complex; it's mainly a matter of taste. If intermediate code generation is interleaved with parsing, one need not build a syntax tree at all (unless of course the syntax tree is the intermediate code). Moreover, it is often possible to write the intermediate code to an output file on the fly, rather than accumulating it in the attributes of the root of the parse tree. The resulting space savings were important for previous generations of computers, which had very small main memories. On the other hand, semantic analysis is easier to perform during a separate traversal of a syntax tree, because that tree reflects the program's semantic structure better than the parse tree does, especially with a top-down parser, and because one has the option of traversing the tree in an order other than that chosen by the parser.
正如有自动工具可以为给定的上下文无关语法构建解析器一样,也有自动工具可以为给定的属性语法构建语义分析器(属性评估器)。属性评估器生成器语法分析已用于基于语法的编辑器 [ RT88 ]、增量编译器 [ SDB84 ]、网页布局 [ MTAB13 ] 以及编程语言研究的各个方面。然而,大多数生产编译器使用临时的手写翻译方案,将解析与语法树的构建交织在一起,在某些情况下,还会交织语义分析或中间代码生成的其他方面。因为它们在解析每个产品时评估其属性,所以它们不需要构建完整的解析树。
Just as there are automatic tools that will construct a parser for a given context-free grammar, there are automatic tools that will construct a semantic analyzer (attribute evaluator) for a given attribute grammar. Attribute evaluator generators have been used in syntax-based editors [RT88], incremental compilers [SDB84], web-page layout [MTAB13], and various aspects of programming language research. Most production compilers, however, use an ad hoc, handwritten translation scheme, interleaving parsing with the construction of a syntax tree and, in some cases, other aspects of semantic analysis or intermediate code generation. Because they evaluate the attributes of each production as it is parsed, they do not need to build the full parse tree.
与解析交错的临时翻译方案采用一组动作例程的形式。动作例程是程序员(语法编写者)指示编译器在解析中的特定点执行的语义函数。大多数解析器生成器都允许程序员指定动作例程。在 LL 解析器生成器中,动作例程可以出现在右侧的任何位置。解析器预测产生式后,将立即调用右侧开头的例程。解析器匹配(产生式)左侧符号后,将立即调用嵌入右侧中间的例程。实现机制很简单:当它预测产生式时,解析器将右侧的所有内容推送到堆栈上,包括终端(要匹配)、非终端(用于驱动未来的预测)和指向动作例程的指针。当它在解析堆栈顶部找到指向动作例程的指针时,解析器只需调用它,并将(指向)相应属性的指针作为参数传递。
An ad hoc translation scheme that is interleaved with parsing takes the form of a set of action routines. An action routine is a semantic function that the programmer (grammar writer) instructs the compiler to execute at a particular point in the parse. Most parser generators allow the programmer to specify action routines. In an LL parser generator, an action routine can appear anywhere within a right-hand side. A routine at the beginning of a right-hand side will be called as soon as the parser predicts the production. A routine embedded in the middle of a right-hand side will be called as soon as the parser has matched (the yield of) the symbol to the left. The implementation mechanism is simple: when it predicts a production, the parser pushes all of the right-hand side onto the stack, including terminals (to be matched), nonterminals (to drive future predictions), and pointers to action routines. When it finds a pointer to an action routine at the top of the parse stack, the parser simply calls it, passing (pointers to) the appropriate attributes as arguments.
在 LR 解析器生成器中,通常不能在右侧的任意位置嵌入动作例程,因为解析器通常只有在看到全部或大部分输出后才知道自己处于什么产生式中。因此,LR 解析器生成器只允许在右侧的部分(后缀)中嵌入动作例程被解析的产生式可以无歧义地标识出来(这称为尾部;有歧义的前缀是左上角)。如果动作例程的属性流严格地自下而上(就像在 S 属性属性语法中一样),那么只需在右侧末尾执行即可。事实上,图 4.1和4.5的属性语法与动作例程版本基本相同。但是,如果动作例程负责很大一部分语义分析(而不是简单地构建语法树),那么它们通常需要上下文信息才能完成工作。为了在 LR 解析中获取和使用这些信息,它们需要一些(必然是有限的)对继承属性或当前产生式之外的信息的访问。我们将在 C-4.5.1 节中进一步讨论这个问题。
In an LR parser generator, one cannot in general embed action routines at arbitrary places in a right-hand side, since the parser does not in general know what production it is in until it has seen all or most of the yield. LR parser generators therefore permit action routines only in the portion (suffix) of the right-hand side in which the production being parsed can be identified unambiguously (this is known as the trailing part; the ambiguous prefix is the left corner). If the attribute flow of the action routines is strictly bottom-up (as it is in an S-attributed attribute grammar), then execution at the end of right-hand sides is all that is needed. The attribute grammars of Figures 4.1 and 4.5, in fact, are essentially identical to the action routine versions. If the action routines are responsible for a significant part of semantic analysis, however (as opposed to simply building a syntax tree), then they will often need contextual information in order to do their job. To obtain and use this information in an LR parse, they will need some (necessarily limited) access to inherited attributes or to information outside the current production. We consider this issue further in Section C-4.5.1.
任何属性评估方法都需要空间来保存语法符号的属性。如果我们要构建显式解析树,那么显而易见的方法是将属性存储在树本身的节点中。如果我们不构建解析树,那么我们需要找到一种方法来跟踪我们已经看到(或预测)但尚未完成解析的符号的属性。自下而上和自上而下的解析器的细节有所不同。
Any attribute evaluation method requires space to hold the attributes of the grammar symbols. If we are building an explicit parse tree, then the obvious approach is to store attributes in the nodes of the tree themselves. If we are not building a parse tree, then we need to find a way to keep track of the attributes for the symbols we have seen (or predicted) but not yet finished parsing. The details differ in bottom-up and top-down parsers.
对于具有 S 属性语法的自下而上的解析器,显而易见的方法是维护一个直接镜像解析堆栈的属性堆栈:解析堆栈上每个状态号旁边都有一个属性记录,记录了我们进入该状态时所移动的符号。解析器驱动程序会自动推送和弹出属性堆栈中的条目;空间管理对操作例程的编写者来说不是问题。如果我们尝试实现继承属性的效果,就会出现复杂情况,但这些可以在基本属性堆栈框架内解决。
For a bottom-up parser with an S-attributed grammar, the obvious approach is to maintain an attribute stack that directly mirrors the parse stack: next to every state number on the parse stack is an attribute record for the symbol we shifted when we entered that state. Entries in the attribute stack are pushed and popped automatically by the parser driver; space management is not an issue for the writer of action routines. Complications arise if we try to achieve the effect of inherited attributes, but these can be accommodated within the basic attribute-stack framework.
对于具有 L 属性语法的自上而下的解析器,我们有两个主要选项。第一个选项是自动的,但比自下而上的语法更复杂。它仍然使用属性堆栈,但不镜像解析堆栈。第二个选项具有较低的空间开销,并通过“缩短”复制规则来节省时间,但需要操作例程明确分配和释放属性的空间。
For a top-down parser with an L-attributed grammar, we have two principal options. The first option is automatic, but more complex than for bottom-up grammars. It still uses an attribute stack, but one that does not mirror the parse stack. The second option has lower space overhead, and saves time by “short-cutting” copy rules, but requires action routines to allocate and deallocate space for attributes explicitly.
在这两种解析器系列中,通常将一些操作例程的上下文信息保存在全局变量中。特别是符号表通常是全局的。我们传递当前活动范围的指示,而不是通过属性将其全部内容从一个产品传递到下一个产品。全局表中的查找然后使用此范围信息来获取正确的引用环境。
In both families of parsers, it is common for some of the contextual information for action routines to be kept in global variables. The symbol table in particular is usually global. Rather than pass its full contents through attributes from one production to the next, we pass an indication of the currently active scope. Lookups in the global table then use this scope information to obtain the right referencing environment.
更深入地
IN MORE DEPTH
我们在配套网站上更详细地讨论了属性空间管理。使用自下而上和自上而下的算术表达式语法,我们说明了自下而上和自上而下的解析器的自动管理,以及自上而下的解析器的临时选项。
We consider attribute space management in more detail on the companion site. Using bottom-up and top-down grammars for arithmetic expressions, we illustrate automatic management for both bottom-up and top-down parsers, as well as the ad hoc option for top-down parsers.
到目前为止,我们在讨论中仅使用属性语法来修饰解析树。正如我们在章节介绍中提到的,属性语法也可用于修饰语法树。如果我们的编译器仅使用动作例程来构建语法树,那么大部分语义分析和中间代码生成将使用语法树作为基础。
In our discussion so far we have used attribute grammars solely to decorate parse trees. As we mentioned in the chapter introduction, attribute grammars can also be used to decorate syntax trees. If our compiler uses action routines simply to build a syntax tree, then the bulk of semantic analysis and intermediate code generation will use the syntax tree as base.
树语法和上下文无关语法在重要方面有所不同。上下文无关语法旨在定义(生成)由标记字符串组成的语言,其中每个字符串都是解析树的边缘(产量)。解析是查找具有给定产量的树的过程。我们在这里使用的树语法旨在定义(或生成)树本身。我们不需要解析的概念:我们可以轻松检查树并确定它是否(以及如何)可以由语法生成。我们介绍树语法的目的是为语法树的装饰提供一个框架。附加到树语法的产生的语义规则可用于定义语法树的属性流,其方式与附加到上下文无关语法的产生的语义规则用于定义解析树的属性流完全相同。我们将在本节的其余部分使用树语法来执行静态语义检查。在第 15 章中,我们将展示如何使用额外的语义规则来生成中间代码。
Tree grammars and context-free grammars differ in important ways. A context-free grammar is meant to define (generate) a language composed of strings of tokens, where each string is the fringe (yield) of a parse tree. Parsing is the process of finding a tree that has a given yield. A tree grammar, as we use it here, is meant to define (or generate) the trees themselves. We have no need for a notion of parsing: we can easily inspect a tree and determine whether (and how) it can be generated by the grammar. Our purpose in introducing tree grammars is to provide a framework for the decoration of syntax trees. Semantic rules attached to the productions of a tree grammar can be used to define the attribute flow of a syntax tree in exactly the same way that semantic rules attached to the productions of a context-free grammar are used to define the attribute flow of a parse tree. We will use a tree grammar in the remainder of this section to perform static semantic checking. In Chapter 15 we will show how additional semantic rules can be used to generate intermediate code.
在我们的示例语法中,我们将错误消息累积到语法树根的合成属性中。在临时属性评估器中,我们可能会试图在发现错误时动态打印这些消息。然而,在实践中,特别是在多遍编译器中,缓冲消息是有意义的,这样它们就可以与编译器其他阶段生成的消息交错,并在编译结束时按程序顺序打印。
In our example grammar we accumulate error messages into a synthesized attribute of the root of the syntax tree. In an ad hoc attribute evaluator we might be tempted to print these messages on the fly as the errors are discovered. In practice, however, particularly in a multipass compiler, it makes sense to buffer the messages, so they can be interleaved with messages produced by other phases of the compiler, and printed in program order at the end of compilation.
可以使用自动属性求值器生成器将我们的属性语法转换为可执行代码。或者,可以以相互递归子例程的形式创建临时求值器(练习 4.20)。在后一种情况下,属性流将在例程的调用序列中明确显示。然后,我们可以根据需要选择将符号表保存在全局变量中,而不是通过属性将其从一个节点传递到另一个节点。大多数编译器都采用临时方法。
One could convert our attribute grammar into executable code using an automatic attribute evaluator generator. Alternatively, one could create an ad hoc evaluator in the form of mutually recursive subroutines (Exercise 4.20). In the latter case attribute flow would be explicit in the calling sequence of the routines. We could then choose if desired to keep the symbol table in global variables, rather than passing it from node to node through attributes. Most compilers employ the ad hoc approach.
本章讨论了语义分析的任务。我们回顾了可分为语法、静态语义和动态语义的语言规则类型,并讨论了是否生成代码来执行动态语义检查的问题。我们还考虑了语义分析器在典型编译器中的作用。我们注意到,静态语义规则的执行和中间代码的生成都可以用解析树或语法树的注释或修饰来表示。然后,我们介绍了属性语法作为此修饰过程的正式框架。
This chapter has discussed the task of semantic analysis. We reviewed the sorts of language rules that can be classified as syntax, static semantics, and dynamic semantics, and discussed the issue of whether to generate code to perform dynamic semantic checks. We also considered the role that the semantic analyzer plays in a typical compiler. We noted that both the enforcement of static semantic rules and the generation of intermediate code can be cast in terms of annotation, or decoration, of a parse tree or syntax tree. We then presented attribute grammars as a formal framework for this decoration process.
属性语法将属性与上下文无关语法或树语法中的每个符号相关联,将属性规则与每个产生式相关联。在 CFG 中,仅在其符号出现在左侧的产生式中计算合成属性。标记的合成属性由扫描器初始化。继承属性在其符号出现在右侧的产生式中计算;它们允许符号下方子树中的计算依赖于符号出现的上下文。起始符号(目标)的继承属性可以表示编译器的外部环境。严格来说,属性语法只允许复制规则(将一个属性分配给另一个属性)和对语义函数的简单调用,但我们通常会放宽此限制以允许某些现有编程语言中或多或少的任意代码片段。
An attribute grammar associates attributes with each symbol in a context-free grammar or tree grammar, and attribute rules with each production. In a CFG, synthesized attributes are calculated only in productions in which their symbol appears on the left-hand side. The synthesized attributes of tokens are initialized by the scanner. Inherited attributes are calculated in productions in which their symbol appears within the right-hand side; they allow calculations in the subtree below a symbol to depend on the context in which the symbol appears. Inherited attributes of the start symbol (goal) can represent the external environment of the compiler. Strictly speaking, attribute grammars allow only copy rules (assignments of one attribute to another) and simple calls to semantic functions, but we usually relax this restriction to allow more or less arbitrary code fragments in some existing programming language.
正如可以根据使用上下文无关语法的解析算法对其进行分类一样,可以根据属性语法的属性流模式的复杂性对其进行分类。S 属性语法(其中所有属性都是合成的)可以自然地在对解析树的一次自下而上的传递中进行评估,其顺序与 LR 系列解析器发现树的顺序完全相同。L 属性语法(其中所有属性流都是从左到右的深度优先)可以按照 LL 系列解析器预测和匹配解析树的顺序进行评估。具有更复杂属性流模式的属性语法通常不用于生产编译器的解析树,但对于基于语法的编辑器、增量编译器和各种其他工具很有价值。
Just as context-free grammars can be categorized according to the parsing algorithm(s) that can use them, attribute grammars can be categorized according to the complexity of their pattern of attribute flow. S-attributed grammars, in which all attributes are synthesized, can naturally be evaluated in a single bottom-up pass over a parse tree, in precisely the order the tree is discovered by an LR-family parser. L-attributed grammars, in which all attribute flow is depth-first left-to-right, can be evaluated in precisely the order that the parse tree is predicted and matched by an LL-family parser. Attribute grammars with more complex patterns of attribute flow are not commonly used for the parse trees of production compilers, but are valuable for syntax-based editors, incremental compilers, and various other tools.
虽然可以构建自动工具来分析属性流和装饰解析树,但是大多数编译器都依赖于动作例程,编译器编写者将其嵌入到产生式的右侧,以评估解析中特定点的属性规则。在 LL 系列解析器中,动作例程可以嵌入到产生式右侧的任意点。在 LR 系列解析器中,动作例程必须遵循产生的左角。自下而上的编译器中属性的空间自然与解析堆栈并行分配,但这使继承属性的管理变得复杂。自上而下的编译器中属性的空间可以自动分配,也可以由动作例程的编写者明确管理。自动方法具有规律性的优势,并且更易于维护;临时方法稍快一些,也更灵活。
While it is possible to construct automatic tools to analyze attribute flow and decorate parse trees, most compilers rely on action routines, which the compiler writer embeds in the right-hand sides of productions to evaluate attribute rules at specific points in a parse. In an LL-family parser, action routines can be embedded at arbitrary points in a production's right-hand side. In an LR-family parser, action routines must follow the production's left corner. Space for attributes in a bottom-up compiler is naturally allocated in parallel with the parse stack, but this complicates the management of inherited attributes. Space for attributes in a top-down compiler can be allocated automatically, or managed explicitly by the writer of action routines. The automatic approach has the advantage of regularity, and is easier to maintain; the ad hoc approach is slightly faster and more flexible.
在单遍编译器中,扫描、解析、语义分析和代码生成交错进行,只对输入进行一次遍历,语义函数或操作例程负责所有的语义分析和代码生成。更常见的是,操作例程只是构建一个语法树,然后在后续遍历的单独遍历中对其进行修饰。这些遍历的代码通常是手写的,以相互递归的子例程的形式,允许编译器适应语法树上基本上任意的属性流。
In a one-pass compiler, which interleaves scanning, parsing, semantic analysis, and code generation in a single traversal of its input, semantic functions or action routines are responsible for all of semantic analysis and code generation. More commonly, action routines simply build a syntax tree, which is then decorated during separate traversal(s) in subsequent pass(es). The code for these traversals is usually written by hand, in the form of mutually recursive subroutines, allowing the compiler to accommodate essentially arbitrary attribute flow on the syntax tree.
在后续章节(特别是第 6-10 章)中,我们将考虑各种编程语言构造。我们不会介绍实现这些构造所需的实际属性语法,而是非正式地描述它们的语义,并给出目标代码的示例。我们将在第 15 章中返回属性语法,届时我们将更详细地考虑中间代码的生成。
In subsequent chapters (6–10 in particular) we will consider a wide variety of programming language constructs. Rather than present the actual attribute grammars required to implement these constructs, we will describe their semantics informally, and give examples of the target code. We will return to attribute grammars in Chapter 15, when we consider the generation of intermediate code in more detail.
4.1 自动机理论的基本结果告诉我们,语言L = a n b n c n = ε , abc , aabbcc , aaabbbccc , … 不是上下文无关的。但是,可以使用属性语法来捕获它。给出一个底层 CFG 和一组属性规则,这些规则将布尔属性ok与每个解析树的根R关联起来,使得当且仅当对应于树边缘的字符串在L中时, R.ok = true。
4.1 Basic results from automata theory tell us that the language L = anbncn = ε, abc, aabbcc, aaabbbccc, … is not context free. It can be captured, however, using an attribute grammar. Give an underlying CFG and a set of attribute rules that associates a Boolean attribute ok with the root R of each parse tree, such that R.ok = true if and only if the string corresponding to the fringe of the tree is in L.
4.2修改 图 2.25中的文法,使其仅接受包含至少一个write语句的程序。对练习 2.17的解答做同样的修改。根据您的经验,您如何看待使用 CFG 来强制执行 C 中每个函数必须包含至少一个return语句的规则?
4.2 Modify the grammar of Figure 2.25 so that it accepts only programs that contain at least one write statement. Make the same change in the solution to Exercise 2.17. Based on your experience, what do you think of the idea of using the CFG to enforce the rule that every function in C must contain at least one return statement?
4.3给出两个 无法以合理成本进行检查的合理语义规则的例子,无论是静态检查还是由编译器在运行时生成的代码进行检查。
4.3 Give two examples of reasonable semantic rules that cannot be checked at reasonable cost, either statically or by compiler-generated code at run time.
4.4根据 示例 4.7中的 CFG 编写一个 S 属性属性语法,将整个表达式的值累积到树的根中。您需要使用动态内存分配,以便各个属性可以容纳任意数量的信息。
4.4 Write an S-attributed attribute grammar, based on the CFG of Example 4.7, that accumulates the value of the overall expression into the root of the tree. You will need to use dynamic memory allocation so that individual attributes can hold an arbitrary amount of information.
4.5 Lisp 具有一个不寻常的特性,即其程序采用带括号的列表的形式。因此,Lisp 程序的自然语法树是一棵二进制单元树(在 Lisp 中称为 cons 单元),其中第一个子节点表示列表的第一个元素,第二个子节点表示列表的其余部分。(cdr '(abc))的语法树如图4.16所示。(符号 ' L是(quote L)的语法糖。)扩展练习 2.18
的 CFG以创建将构建此类树的属性语法。当一棵解析树完全修饰后,根应该有一个引用语法树的属性v。你可以假设每个原子都有一个合成属性v,它引用一个保存来自扫描器的信息的语法树节点。在你的语义函数中,你可以假设有一个cons函数,它以两个引用作为参数,并返回一个包含这些引用的新 cons 单元的引用。
4.5 Lisp has the unusual property that its programs take the form of parenthesized lists. The natural syntax tree for a Lisp program is thus a tree of binary cells (known in Lisp as cons cells), where the first child represents the first element of the list and the second child represents the rest of the list. The syntax tree for (cdr '(a b c)) appears in Figure 4.16. (The notation 'L is syntactic sugar for (quote L).)
Extend the CFG of Exercise 2.18 to create an attribute grammar that will build such trees. When a parse tree has been fully decorated, the root should have an attribute v that refers to the syntax tree. You may assume that each atom has a synthesized attribute v that refers to a syntax tree node that holds information from the scanner. In your semantic functions, you may assume the availability of a cons function that takes two references as arguments and returns a reference to a new cons cell containing those references.
4.6 回顾练习 2.13中的上下文无关文法。向文法中添加属性规则,将程序字符串中括号嵌套的最大深度计数累积到树的根中。例如,给定字符串f1(a, f2(b * (c + (d − (e − f))))),树根处的语句应具有计数为 3 的属性(参数列表周围的括号不计算在内)。
4.6 Refer back to the context-free grammar of Exercise 2.13. Add attribute rules to the grammar to accumulate into the root of the tree a count of the maximum depth to which parentheses are nested in the program string. For example, given the string f1(a, f2(b * (c + (d − (e − f))))),the stmt at the root of the tree should have an attribute with a count of 3 (the parentheses surrounding argument lists don't count).
4.7 假设我们要将常量表达式翻译成后缀表示法,即逻辑学家 Jan Lukasiewicz 的“逆波兰”表示法。后缀表示法不需要括号。它出现在基于堆栈的语言中,如 Postscript、Forth 以及1.4 节中提到的 P 码和 Java 字节码中间形式。在历史上,它也曾用作惠普生产的某些手持计算器的输入语言。当给定一个数字时,后缀计算器会将该数字压入内部堆栈。当给定一个运算符时,它会从堆栈中弹出前两个数字,应用运算符,然后压入结果。显示屏会在堆栈顶部显示该值。例如,要计算 2 × (15 − 3)/4,需要压入 2 1 5
3
− * 4
/ (这里
是“enter”键,用于结束构成数字的数字串)。使用图 4.1
的底层 CFG ,编写一个属性语法,将解析树的根与后缀计算器按钮按下序列seq关联起来,该序列将计算从该符号派生的标记的算术值。您可以假设存在一个函数buttons(c) ,它返回常数c的按钮按下序列(以后缀计算器结尾)。您还可以假设存在一个用于按钮按下序列的连接函数。
4.7 Suppose that we want to translate constant expressions into the postfix, or “reverse Polish” notation of logician Jan Lukasiewicz. Postfix notation does not require parentheses. It appears in stack-based languages such as Postscript, Forth, and the P-code and Java bytecode intermediate forms mentioned in Section 1.4. It also served, historically, as the input language of certain hand-held calculators made by Hewlett-Packard. When given a number, a postfix calculator would push the number onto an internal stack. When given an operator, it would pop the top two numbers from the stack, apply the operator, and push the result. The display would show the value at the top of the stack. To compute 2 × (15 − 3)/4, for example, one would push 2 1 5 3 − * 4 / (here is the “enter” key, used to end the string of digits that constitute a number).
Using the underlying CFG of Figure 4.1, write an attribute grammar that will associate with the root of the parse tree a sequence of postfix calculator button pushes, seq, that will compute the arithmetic value of the tokens derived from that symbol. You may assume the existence of a function buttons(c) that returns a sequence of button pushes (ending with on a postfix calculator) for the constant c. You may also assume the existence of a concatenation function for sequences of button pushes.
4.8使用 图 4.3的底层 CFG 重复前面的练习。
4.8 Repeat the previous exercise using the underlying CFG of Figure 4.3.
4.9 考虑以下逆波兰算术表达式的文法:E → EE op | id op → + | − | * | /假设每个 id 都有一个字符串类型的合成属性名,并且每个E和op都有一个字符串类型的属性val,编写一个属性文法,安排解析树根的val属性包含将表达式翻译成传统的中缀表示法。例如,如果树的叶子从左到右为“AAB − * C /”,则根的 val 字段为“( (A*(A − B))/C ) ”。作为额外的挑战,编写一个属性文法版本,利用通常的算术优先级和结合性规则尽可能少地使用括号。
4.9 Consider the following grammar for reverse Polish arithmetic expressions:
E → E E op | id
op → + | − | * | /
Assuming that each id has a synthesized attribute name of type string, and that each E and op has an attribute val of type string, write an attribute grammar that arranges for the val attribute of the root of the parse tree to contain a translation of the expression into conventional infix notation. For example, if the leaves of the tree, left to right, were “A A B − * C /,” then the val field of the root would be “( (A*(A − B))/C ).” As an extra challenge, write a version of your attribute grammar that exploits the usual arithmetic precedence and associativity rules to use as few parentheses as possible.
4.10 为了减少印刷错误的可能性,大多数信用卡号的数字都设计成满足所谓的Luhn 公式,该公式由 ANSI 在 20 世纪 60 年代标准化,以 IBM 数学家 Hans Peter Luhn 的名字命名。从右边开始,我们将每隔一位数字加倍(倒数第二位、倒数第四位等)。如果加倍后的数值为 10 或更大,我们将结果数字。然后我们将所有数字相加。对于任何有效数字,结果都是 10 的倍数。例如,1234 5678 9012 3456 变为 2264 1658 9022 6416,总和为 64,因此这不是有效数字。但是,如果最后一位数字是 2,总和将是 60,因此该数字可能是有效的。
为数字字符串提供一个属性语法,该属性语法会累积到解析树的根中,并根据 Luhn 公式指示该字符串是否有效。您的语法应该适应任意长度的字符串。
4.10 To reduce the likelihood of typographic errors, the digits comprising most credit card numbers are designed to satisfy the so-called Luhn formula, standardized by ANSI in the 1960s, and named for IBM mathematician Hans Peter Luhn. Starting at the right, we double every other digit (the second-to-last, fourth-to-last, etc.). If the doubled value is 10 or more, we add the resulting digits. We then sum together all the digits. In any valid number the result will be a multiple of 10. For example, 1234 5678 9012 3456 becomes 2264 1658 9022 6416, which sums to 64, so this is not a valid number. If the last digit had been 2, however, the sum would have been 60, so the number would potentially be valid.
Give an attribute grammar for strings of digits that accumulates into the root of the parse tree a Boolean value indicating whether the string is valid according to Luhn's formula. Your grammar should accommodate strings of arbitrary length.
4.11 考虑以下浮点常量的 CFG,不带指数表示法。(请注意,这个练习有些不自然:所讨论的语言是常规语言,可以由典型编译器的扫描器处理。)C →数字.数字数字→数字 更多数字更多数字→数字| ε数字→ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9用属性规则扩充此语法,这些规则会将常量的值累积到解析树根的 val 属性中。您的答案应该是 S 属性。
4.11 Consider the following CFG for floating-point constants, without exponential notation. (Note that this exercise is somewhat artificial: the language in question is regular, and would be handled by the scanner of a typical compiler.)
C → digits . digits
digits → digit more_digits
more_digits → digits | ε
digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Augment this grammar with attribute rules that will accumulate the value of the constant into a val attribute of the root of the parse tree. Your answer should be S-attributed.
4.12 对上一个问题的明显解决方案的一个潜在批评是,解析树内部节点的值不反映上下文中其下方边缘的值。创建一个替代解决方案来解决这个批评。更具体地说,以这样的方式创建语法,即内部节点的值是其子节点的值的总和。通过绘制12.34的解析树和属性流来说明您的解决方案。(提示:您可能需要不同的底层 CFG 和非 L 属性流。)
4.12 One potential criticism of the obvious solution to the previous problem is that the values in internal nodes of the parse tree do not reflect the value, in context, of the fringe below them. Create an alternative solution that addresses this criticism. More specifically, create your grammar in such a way that the val of an internal node is the sum of the vals of its children. Illustrate your solution by drawing the parse tree and attribute flow for 12.34. (Hint: You will probably want a different underlying CFG, and non-L-attributed flow.)
4.13 根据练习 2.11的 CFG,考虑以下变量声明的属性语法:decl → ID decl_tail decl.t := decl_tail.t decl_tail.in_tab := insert (decl.in_tab, ID.n, decl_tail.t) decl.out_tab := decl_tail.out_tab decl_tail →, decl decl_tail.t := decl.t decl.in_tab := decl_tail.in_tab decl_tail.out_tab := decl.out_tab decl_tail −→ : ID ; decl_tail.t := ID.n decl_tail.out_tab := decl_tail.in_tab
显示字符串A, B : C ; 的解析树。然后,使用箭头和文本描述,指定完全装饰树所需的属性流。(提示:请注意,语法不是L属性的。)
4.13 Consider the following attribute grammar for variable declarations, based on the CFG of Exercise 2.11:
decl → ID decl_tail
decl.t := decl_tail.t
decl_tail.in_tab := insert (decl.in_tab, ID.n, decl_tail.t)
decl.out_tab := decl_tail.out_tab
decl_tail →, decl
decl_tail.t := decl.t
decl.in_tab := decl_tail.in_tab
decl_tail.out_tab := decl.out_tab
decl_tail −→ : ID ;
decl_tail.t := ID.n
decl_tail.out_tab := decl_tail.in_tab
Show a parse tree for the string A, B : C;. Then, using arrows and textual description, specify the attribute flow required to fully decorate the tree. (Hint: Note that the grammar is not L-attributed.)
4.14 能够处理非 L 属性属性流的基于 CFG 的属性评估器需要以解析树作为输入。解释如何在自上而下或自下而上的解析过程中自动构建解析树(即,无需显式操作例程)。
4.14 A CFG-based attribute evaluator capable of handling non-L-attributed attribute flow needs to take a parse tree as input. Explain how to build a parse tree automatically during a top-down or bottom-up parse (i.e., without explicit action routines).
4.15在 例 4.13 的基础上,修改图 2.17的递归下降解析器的其余部分,以构建计算器语言程序的语法树。
4.15 Building on Example 4.13, modify the remainder of the recursive descent parser of Figure 2.17 to build syntax trees for programs in the calculator language.
4.16编写一个具有动作例程和自动属性空间管理的 LL(1) 语法,以生成 练习 4.7中描述的逆波兰翻译。
4.16 Write an LL(1) grammar with action routines and automatic attribute space management that generates the reverse Polish translation described in Exercise 4.7.
4.17
4.17
(a)为 x中的多项式写一个上下文无关文法。添加语义函数来生成一个属性文法,该属性文法会将多项式的导数(作为字符串)累积在解析树根的合成属性中。
(a) Write a context-free grammar for polynomials in x. Add semantic functions to produce an attribute grammar that will accumulate the polynomial's derivative (as a string) in a synthesized attribute of the root of the parse tree.
(b)用 可以在解析期间评估的动作例程替换语义函数。
(b) Replace your semantic functions with action routines that can be evaluated during parsing.
4.18
4.18
(a) 用 Pascal 或 C 风格编写case或switch语句的上下文无关文法。添加语义函数以确保相同的标签不会出现在构造的两个不同臂上。
(a) Write a context-free grammar for case or switch statements in the style of Pascal or C. Add semantic functions to ensure that the same label does not appear on two different arms of the construct.
(b)用 可以在解析期间评估的动作例程替换语义函数。
(b) Replace your semantic functions with action routines that can be evaluated during parsing.
4.19 编写一个算法来确定任意属性文法的规则是否为非循环的。(在最坏的情况下,你的算法将需要指数时间[ JOR75 ]。)
4.19 Write an algorithm to determine whether the rules of an arbitrary attribute grammar are noncircular. (Your algorithm will require exponential time in the worst case [JOR75].)
4.20用您喜欢的编程语言将 图 4.14中的属性文法重写为由相互递归子程序组成的临时树遍历形式。将符号表保存在全局变量中,而不是通过参数传递它。
4.20 Rewrite the attribute grammar of Figure 4.14 in the form of an ad hoc tree traversal consisting of mutually recursive subroutines in your favorite programming language. Keep the symbol table in a global variable, rather than passing it through arguments.
4.21 根据图 4.11的 CFG 编写一个属性语法,它将构建一棵具有图 4.14中所述结构的语法树。
4.21 Write an attribute grammar based on the CFG of Figure 4.11 that will build a syntax tree with the structure described in Figure 4.14.
4.22扩充 图 4.5、图 4.6或练习 4.21中的属性语法,在每个语法树节点中初始化一个合成属性,该属性指示相应结构在源程序中出现的位置(行和列)。您可以假设扫描器初始化每个标记的位置。
4.22 Augment the attribute grammar of Figure 4.5, Figure 4.6, or Exercise 4.21 to initialize a synthesized attribute in every syntax tree node that indicates the location (line and column) at which the corresponding construct appears in the source program. You may assume that the scanner initializes the location of every token.
4.23修改 图 4.11和4.14中的 CFG 和属性语法,以允许混合整数和实数表达式,而无需float和trunc。您将需要向必须强制转换为相反类型的任何节点添加注释,以便代码生成器知道生成代码来执行所以。一定要仔细考虑你的强制转换规则。例如,在表达式my_int + my_real中,你如何知道是否要将整数强制转换为实数,还是将实数强制转换为整数?
4.23 Modify the CFG and attribute grammar of Figures 4.11 and 4.14 to permit mixed integer and real expressions, without the need for float and trunc. You will want to add an annotation to any node that must be coerced to the opposite type, so that the code generator will know to generate code to do so. Be sure to think carefully about your coercion rules. In the expression my_int + my_real, for example, how will you know whether to coerce the integer to be a real, or to coerce the real to be an integer?
4.24解释 树形文法中产生式左侧需要A;B符号的原因。为什么上下文无关文法不需要类似的符号?
4.24 Explain the need for the A ; B notation on the left-hand sides of productions in a tree grammar. Why isn't similar notation required for context-free grammars?
4.25 示例 4.17中的树属性语法的一个潜在缺点是,它反复将整个符号表从一个节点复制到另一个节点。在这个特定的微型语言中,很容易看出引用环境永远不会缩小:符号表只会随着新标识符的添加而改变。利用这一观察,说明如何修改图 4.14中的伪代码,使其仅复制指针,而不是整个符号表。
4.25 A potential objection to the tree attribute grammar of Example 4.17 is that it repeatedly copies the entire symbol table from one node to another. In this particular tiny language, it is easy to see that the referencing environment never shrinks: the symbol table changes only with the addition of new identifiers. Exploiting this observation, show how to modify the pseudocode of Figure 4.14 so that it copies only pointers, rather than the entire symbol table.
4.26 您对上一个练习的解决方案可能不适用于具有非平凡作用域规则的语言。解释如何修改图 4.14中的 AG以使用类似于第 C-3.4.1 节中描述的全局符号表。除其他事项外,您还应考虑嵌套作用域、在外部作用域中隐藏名称以及在使用变量之前声明变量的要求(第 C-3.4.1 节的表格未强制执行)。
4.26 Your solution to the previous exercise probably doesn't generalize to languages with nontrivial scoping rules. Explain how an AG such as that in Figure 4.14 might be modified to use a global symbol table similar to the one described in Section C-3.4.1. Among other things, you should consider nested scopes, the hiding of names in outer scopes, and the requirement (not enforced by the table of Section C-3.4.1) that variables be declared before they are used.
4.27–4.31 更深入。
4.27–4.31 In More Depth.
4.32 属性语法最具影响力的应用之一是 Cornell 合成器生成器 [ Rep84,RT88 ]。了解生成器如何使用属性语法不仅对编辑中的程序中的语义信息进行增量更新,而且还根据形式语言规范自动创建基于语言的编辑器。这种技术有多通用?除了语法制导的计算机程序编辑之外,它还可能有哪些应用?
4.32 One of the most influential applications of attribute grammars was the Cornell Synthesizer Generator [Rep84, RT88]. Learn how the Generator used attribute grammars not only for incremental update of semantic information in a program under edit, but also for automatic creation of language based editors from formal language specifications. How general is this technique? What applications might it have beyond syntax-directed editing of computer programs?
4.33 本章中使用的属性语法都非常简单。大多数是 S 或 L 属性的。所有都是非循环的。更复杂的属性语法有什么实际用途吗?自动属性评估器怎么样?使用书目注释作为起点,对属性评估技术进行调查。实用技术和求知欲之间的界限在哪里?
4.33 The attribute grammars used in this chapter are all quite simple. Most are S- or L-attributed. All are noncircular. Are there any practical uses for more complex attribute grammars? How about automatic attribute evaluators? Using the Bibliographic Notes as a starting point, conduct a survey of attribute evaluation techniques. Where is the line between practical techniques and intellectual curiosities?
4.34 第一个经过验证的 Ada 实现是纽约大学 [ DGAFS + 80 ] 的 Ada/Ed 解释器。该解释器用基于集合的语言 SETL [ SDDS86 ] 编写,使用了 Ada 的指称语义定义。了解 Ada/Ed 项目、SETL 和指称语义。讨论使用形式定义如何帮助开发过程。还讨论了 Ada/Ed 的局限性,并扩展了形式语义在语言设计、开发和原型实现中的潜在作用。
4.34 The first validated Ada implementation was the Ada/Ed interpreter from New York University [DGAFS+80]. The interpreter was written in the set-based language SETL [SDDS86] using a denotational semantics definition of Ada. Learn about the Ada/Ed project, SETL, and denotational semantics. Discuss how the use of a formal definition aided the development process. Also discuss the limitations of Ada/Ed, and expand on the potential role of formal semantics in language design, development, and prototype implementation.
4.35 Scheme 语言手册 [ KCR + 98 ] 的第 5 版包含了 Scheme 在指称语义中的正式定义。与更传统的英语定义相比,这个定义有多长?它的可读性如何?长度和可读性水平说明了 Scheme 的什么问题?关于指称语义?(有关指称语义的更多信息,请参阅 Stoy [ Sto77 ] 或 Gordon [ Gor79 ] 的文章。)
手册的第 6 版 [ SDF + 07 ] 切换到操作语义。与指称版本相比如何?你认为标准委员会为什么做出这一改变?(有关更多信息,请参阅 Matthews 和 Findler 的论文 [ MF08 ]。)
4.35 Version 5 of the Scheme language manual [KCR+98] included a formal definition of Scheme in denotational semantics. How long is this definition, compared to the more conventional definition in English? How readable is it? What do the length and the level of readability say about Scheme? About denotational semantics? (For more on denotational semantics, see the texts of Stoy [Sto77] or Gordon [Gor79].)
Version 6 of the manual [SDF+07] switched to operational semantics. How does this compare to the denotational version? Why do you suppose the standards committee made the change? (For more information, see the paper by Matthews and Findler [MF08].)
4.36–4.37 更深入。
4.36–4.37 In More Depth.
属性语法的早期理论大部分是由 Knuth [ Knu68 ] 开发的。Lewis、Rosenkrantz 和 Stearns [ LRS74 ] 引入了 L 属性语法的概念。Watt [ Wat77 ] 展示了如何在自下而上的解析器中使用标记符号模拟继承的属性。Jazayeri、Ogden 和 Rounds [ JOR75 ] 表明,在最坏的情况下,用任意属性流来装饰解析树可能需要指数级的时间。Courcelle [ Cou84 ] 和 Engelfriet [ Eng84 ] 的文章概述了属性评估的理论和实践。
Much of the early theory of attribute grammars was developed by Knuth [Knu68]. Lewis, Rosenkrantz, and Stearns [LRS74] introduced the notion of an L-attributed grammar. Watt [Wat77] showed how to use marker symbols to emulate inherited attributes in a bottom-up parser. Jazayeri, Ogden, and Rounds [JOR75] showed that exponential time may be required in the worst case to decorate a parse tree with arbitrary attribute flow. Articles by Courcelle [Cou84] and Engelfriet [Eng84] survey the theory and practice of attribute evaluation.
基于属性语法的语言程序编辑是由Reps 和 Teitelbaum 的Synthesizer Generator [ RT88 ](特定于语言的 Cornell 程序合成器 [ TR81 ]的后续产品)率先提出的。Magpie [ SDB84 ] 是一个早期的增量编译器,同样基于属性语法。Meyerovich 等人 [ MTAB13 ] 最近使用属性语法并行化了各种树遍历任务,尤其是用于网页渲染和 GPU 加速动画。
Language-based program editing based on attribute grammars was pioneered by the Synthesizer Generator [RT88] (a follow-on to the language-specific Cornell Program Synthesizer [TR81]) of Reps and Teitelbaum. Magpie [SDB84] was an early incremental compiler, again based on attribute grammars. Meyerovich et al. [MTAB13] have recently used attribute grammars to parallelize a variety of tree-traversal tasks—notably for web page rendering and GPU-accelerated animation.
在 Fischer 等人或 Appel [ App97 ]的文本中可以找到实现许多语言特性的动作例程。有关属性语法的更多注释,可以在 Cooper 和 Torczon [ CT04,第 171-188 页] 或 Aho 等人 [ ALSU07,第 5 章]的文本中找到。
Action routines to implement many language features can be found in the texts of Fischer et al. or Appel [App97]. Further notes on attribute grammars can be found in the texts of Cooper and Torczon [CT04, pp. 171–188] or Aho et al. [ALSU07, Chap. 5].
Marcotty、Ledgard 和 Bochmann [ MLB76 ] 提供了编程语言语义形式化符号的早期概述。在 Winskel [ Win93 ] 和 Slonneger 和 Kurtz [ SK95 ] 的文本中可以找到更详细但仍有些过时的处理。Nipkow 和 Klein 从现代和数学严谨的角度介绍了该主题,将他们的文本与可执行定理证明系统 [ NK15 ] 相结合。
Marcotty, Ledgard, and Bochmann [MLB76] provide an early survey of formal notations for programming language semantics. More detailed but still somewhat dated treatment can be found in the texts of Winskel [Win93] and Slonneger and Kurtz [SK95]. Nipkow and Klein cover the topic from a modern and mathematically rigorous perspective, integrating their text with an executable theorem-proving system [NK15].
关于公理语义学的开创性论文由 Hoare [ Hoa69 ] 撰写。关于该主题的一本优秀书籍是 Gries 的《编程科学》 [ Gri81 ]。关于指称语义学的开创性论文由 Scott 和 Strachey [ SS71 ] 撰写。关于该主题的早期文本包括 Stoy [ Sto77 ] 和 Gordon [ Gor79 ]的文本。
The seminal paper on axiomatic semantics is by Hoare [Hoa69]. An excellent book on the subject is Gries's The Science of Programming [Gri81]. The seminal paper on denotational semantics is by Scott and Strachey [SS71]. Early texts on the subject include those of Stoy [Sto77] and Gordon [Gor79].
如第 1 章所述,编译器只是一个翻译器。它将用一种语言编写的程序翻译成用另一种语言编写的程序。第二种语言几乎可以是任何东西——其他一些高级语言、照相排版命令、VLSI(芯片)布局——但大多数时候它是某些可用计算机的机器语言。
As described in Chapter 1, a compiler is simply a translator. It translates programs written in one language into programs written in another language. This second language can be almost anything—some other high-level language, phototypesetting commands, VLSI (chip) layouts—but most of the time it's the machine language for some available computer.
正如存在许多不同的编程语言一样,机器语言也有许多不同的语言,尽管后者的多样性往往远低于前者。每种机器语言都对应一种不同的处理器体系结构。形式上,体系结构是硬件和软件之间的接口——软件是由编译器生成的语言,或由程序员为裸机编写的语言。处理器的实现是体系结构的具体实现,通常是硬件。要生成正确的代码,编译器编写者只要了解目标体系结构就足够了。要生成快速的代码,通常还需要了解实现,因为实现决定了给定语言结构的备选翻译的相对速度。
Just as there are many different programming languages, there are many different machine languages, though the latter tend to display considerably less diversity than the former. Each machine language corresponds to a different processor architecture. Formally, an architecture is the interface between the hardware and the software—that is, the language generated by a compiler, or by a programmer writing for the bare machine. The implementation of the processor is a concrete realization of the architecture, generally in hardware. To generate correct code, it suffices for a compiler writer to understand the target architecture. To generate fast code, it is generally necessary to understand the implementation as well, because it is the implementation that determines the relative speeds of alternative translations of a given language construct.
更深入地
IN MORE DEPTH
第 5 章的完整内容可以在配套网站上找到。它简要概述了处理器架构和实现方面,这些方面对编译器编写者来说特别重要,甚至可能值得以前看过该材料的读者复习一下。主要主题包括数据表示、指令集架构、实现技术的演变以及为现代处理器编译的挑战。示例主要来自 x86(一种主导台式机/笔记本电脑市场的传统 CISC(复杂指令集)架构)和 ARM(一种主导嵌入式、智能手机和平板电脑市场的更现代的 RISC(精简指令集)设计)。
Chapter 5 can be found in its entirety on the companion site. It provides a brief overview of those aspects of processor architecture and implementation of particular importance to compiler writers, and maybe worth reviewing even by readers who have seen the material before. Principal topics include data representation, instruction set architecture, the evolution of implementation techniques, and the challenges of compiling for modern processors. Examples are drawn largely from the x86, a legacy CISC (complex instruction set) architecture that dominates the desktop/laptop market, and the ARM, a more modern RISC (reduced instruction set) design that dominates the embedded, smart phone, and tablet markets.
语言设计的核心问题
Core Issues in Language Design
在第一部分奠定了基础之后,我们现在来讨论大多数编程语言的核心问题:控制流、数据类型以及控制和数据的抽象。
Having laid the foundation in Part I, we now turn to issues that lie at the core of most programming languages: control flow, data types, and abstractions of both control and data.
第 6 章讨论控制流,包括表达式求值、排序、选择、迭代和递归。在许多情况下,我们将看到设计决策反映了有时互补但往往相互竞争的目标,即概念清晰和高效实现。几个问题,包括引用和值之间的区别以及应用(急切)求值和惰性求值之间的区别,将在后面的章节中再次出现。
Chapter 6 considers control flow, including expression evaluation, sequencing, selection, iteration, and recursion. In many cases we will see design decisions that reflect the sometimes complementary but often competing goals of conceptual clarity and efficient implementation. Several issues, including the distinction between references and values and between applicative (eager) and lazy evaluation, will recur in later chapters.
接下来的两章讨论了类型问题。 第 7 章介绍了类型系统和类型检查,包括等价性、兼容性和类型推断的概念。它还讨论了参数多态性问题,包括其隐式和显式(通用)形式。然后,第 8 章概述了具体的复合类型,包括记录和变体、数组、字符串、集合、指针、列表和文件。关于指针的部分介绍了垃圾收集技术。
The next two chapters consider the subject of types. Chapter 7 covers type systems and type checking, including the notions of equivalence, compatibility, and inference of types. It also considers the subject of parametric polymorphism, in both its implicit and explicit (generic) forms. Chapter 8 then presents a survey of concrete composite types, including records and variants, arrays, strings, sets, pointers, lists, and files. The section on pointers includes an introduction to garbage collection techniques.
控制和数据都易于抽象,即在简单且定义良好的接口背后隐藏复杂性的过程。控制抽象是第 9 章的主题。子程序是最常见的控制抽象,但我们也会考虑异常和协同程序,并简要回顾第 6 章中介绍的延续和迭代器主题。子程序的介绍侧重于调用序列和参数传递机制。
Both control and data are amenable to abstraction, the process whereby complexity is hidden behind a simple and well-defined interface. Control abstraction is the subject of Chapter 9. Subroutines are the most common control abstraction, but we also consider exceptions and coroutines, and return briefly to the subjects of continuations and iterators, introduced in Chapter 6. The coverage ofsubroutines focuses on calling sequences and on parameter-passing mechanisms.
第 10 章回归第 3 章中介绍的数据抽象主题。在许多现代语言中,该主题采用面向对象的形式,其特点是封装机制、继承和动态方法分派(子类型多态性)。我们对面向对象语言的介绍还将涉及构造函数、访问控制、泛型、闭包以及混合和多重继承。
Chapter 10 returns to the subject of data abstraction, introduced in Chapter 3. In many modern languages this subject takes the form of object orientation, characterized by an encapsulation mechanism, inheritance, and dynamic method dispatch (subtype polymorphism). Our coverage of object-oriented languages will also touch on constructors, access control, generics, closures, and mix-in and multiple inheritance.
讨论了编译器用来执行语义规则的机制(第 4 章)以及编译器必须为其生成代码的目标机器的特征(第 5 章)之后,我们现在回到语言设计的核心问题。具体来说,我们在本章中讨论程序执行中的控制流或顺序问题。顺序是大多数计算模型的基础。它决定了应该先做什么,然后做什么,等等,以完成某项期望的任务。我们可以将用于指定顺序的语言机制分为几类:
Having considered the mechanisms that a compiler uses to enforce semantic rules (Chapter 4) and the characteristics of the target machines for which compilers must generate code (Chapter 5), we now return to core issues in language design. Specifically, we turn in this chapter to the issue of control flow or ordering in program execution. Ordering is fundamental to most models of computing. It determines what should be done first, what second, and so forth, to accomplish some desired task. We can organize the language mechanisms used to specify ordering into several categories:
1. 排序:语句要按照特定的顺序执行(或表达式求值),通常是它们在程序文本中出现的顺序。
1. Sequencing: Statements are to be executed (or expressions evaluated) in a certain specified order—usually the order in which they appear in the program text.
2. 选择:根据某些运行时条件,在两个或多个语句或表达式之间进行选择。最常见的选择结构是if和case (switch)语句。选择有时也称为交替。
2. Selection: Depending on some run-time condition, a choice is to be made among two or more statements or expressions. The most common selection constructs are if and case (switch) statements. Selection is also sometimes referred to as alternation.
3. 迭代:一段给定的代码将被重复执行,要么执行一定次数,要么直到某个运行时条件为真。迭代结构包括for/do、while和repeat循环。
3. Iteration: A given fragment of code is to be executed repeatedly, either a certain number of times, or until a certain run-time condition is true. Iteration constructs include for/do, while, and repeat loops.
4. 程序抽象:将一组可能复杂的控制结构(子程序)封装起来,使之可以被视为一个单元,通常可以进行参数化。
4. Procedural abstraction: A potentially complex collection of control constructs (a subroutine) is encapsulated in a way that allows it to be treated as a single unit, usually subject to parameterization.
5. 递归:表达式直接或间接地根据其自身(更简单的版本)进行定义;计算模型需要一个堆栈来保存有关表达式部分求值实例的信息。递归通常通过自引用子程序来定义。
5. Recursion: An expression is defined in terms of (simpler versions of) itself, either directly or indirectly; the computational model requires a stack on which to save information about partially evaluated instances of the expression. Recursion is usually defined by means of self-referential subroutines.
6. 并发性:两个或多个程序片段需要“同时”执行/评估,可以在单独的处理器上并行执行,也可以在单个处理器上交错执行,以实现相同的效果。
6. Concurrency: Two or more program fragments are to be executed/evaluated “at the same time,” either in parallel on separate processors, or interleaved on a single processor in a way that achieves the same effect.
7. 异常处理和推测:程序片段以乐观的方式执行,假设某些预期条件将为真。如果该条件结果为假,执行分支到处理程序,该处理程序代替受保护片段的剩余部分执行(在异常处理的情况下),或代替整个受保护片段执行(在推测的情况下)。对于推测,语言实现必须能够撤消或“回滚”受保护代码的任何可见效果。
7. Exception handling and speculation: A program fragment is executed optimistically, on the assumption that some expected condition will be true. If that condition turns out to be false, execution branches to a handler that executes in place of the remainder of the protected fragment (in the case of exception handling), or in place of the entire protected fragment (in the case of speculation). For speculation, the language implementation must be able to undo, or “roll back,” any visible effects of the protected code.
8. 不确定性:语句或表达式之间的顺序或选择故意不明确,这意味着任何替代方案都会产生正确的结果。有些语言要求选择是随机的,或者说是公平的,从某种正式意义上来说。
8. Nondeterminacy: The ordering or choice among statements or expressions is deliberately left unspecified, implying that any alternative will lead to correct results. Some languages require the choice to be random, or fair, in some formal sense of the word.
虽然语法和语义细节因语言而异,但这些类别涵盖了大多数编程语言中的所有控制流构造和机制。如果程序员能够按照这些类别而不是某种特定语言的语法来思考,那么他们就会发现学习新语言、评估语言之间的权衡以及以独立于语言的方式设计和推理算法会很容易。
Though the syntactic and semantic details vary from language to language, these categories cover all of the control-flow constructs and mechanisms found in most programming languages. A programmer who thinks in terms of these categories, rather than the syntax of some particular language, will find it easy to learn new languages, evaluate the tradeoffs among languages, and design and reason about algorithms in a language-independent way.
子程序是第 9 章的主题。并发是第 13 章的主题。这两章也讨论了异常处理和推测,分别在第 9.4 节和13.4.4节。本章的大部分内容(第 6.3 节至6.7节)用于讨论剩下的五个类别。我们从第 6.1 节开始考虑表达式的求值— 所有高级排序都基于此构建块。我们考虑表达式的句法形式、运算符的优先级和结合性、操作数的求值顺序以及赋值语句的语义。我们特别关注保存值的变量和保存对值的引用的变量之间的区别;这种区别在未来的章节中会多次发挥重要作用。在第 6.2 节中,我们考虑结构化和非结构化(基于 goto)控制流之间的区别。
Subroutines are the subject of Chapter 9. Concurrency is the subject of Chapter 13. Exception handling and speculation are discussed in those chapters as well, in Sections 9.4 and 13.4.4. The bulk of the current chapter (Sections 6.3 through 6.7) is devoted to the five remaining categories. We begin in Section 6.1 by considering the evaluation of expressions—the building blocks on which all higher-level ordering is based. We consider the syntactic form of expressions, the precedence and associativity of operators, the order of evaluation of operands, and the semantics of the assignment statement. We focus in particular on the distinction between variables that hold a value and variables that hold a reference to a value; this distinction will play an important role many times in future chapters. In Section 6.2 we consider the difference between structured and unstructured (goto-based) control flow.
不同类别的控制流的相对重要性在不同类别的编程语言中存在很大差异。排序是命令式(冯·诺依曼和面向对象)语言的核心,但在函数式语言中却起着相对较小的作用,函数式语言强调表达式的求值,不强调或消除以除返回值以外的任何方式影响程序输出的语句(例如赋值)。同样,函数式语言大量使用递归,而命令式语言则倾向于强调迭代。逻辑语言倾向于完全不强调或隐藏控制流问题:程序员只需指定一组推理规则;语言实现必须找到应用这些规则的顺序,以便推导出满足某些所需属性的值。
The relative importance of different categories of control flow varies significantly among the different classes of programming languages. Sequencing is central to imperative (von Neumann and object-oriented) languages, but plays a relatively minor role in functional languages, which emphasize the evaluation of expressions, de-emphasizing or eliminating statements (e.g., assignments) that affect program output in any way other than through the return of a value. Similarly, functional languages make heavy use of recursion, while imperative languages tend to emphasize iteration. Logic languages tend to de-emphasize or hide the issue of control flow entirely: The programmer simply specifies a set of inference rules; the language implementation must find an order in which to apply those rules that will allow it to deduce values that satisfy some desired property.
通常,一种语言可以指定函数调用(运算符调用)使用前缀、中缀或后缀表示法。这些术语分别表示函数名称出现在其几个参数之前、之中还是之后:
In general, a language may specify that function calls (operator invocations) employ prefix, infix, or postfix notation. These terms indicate, respectively, whether the function name appears before, among, or after its several arguments:
Postscript、Forth(某些手持计算器的输入语言)和某些编译器的中间代码中的大多数函数都使用后缀表示法。后缀也出现在其他语言的几个地方。示例包括 Pascal 的指针解引用运算符 (S) 以及 C 及其后代的后置增量和减量运算符 (++ 和 −−)。
Postfix notation is used for most functions in Postscript, Forth, the input language of certain hand-held calculators, and the intermediate code of some compilers. Postfix appears in a few places in other languages as well. Examples include the pointer dereferencing operator(S) of Pascal and the post-increment and decrement operators (++ and −−) of C and its descendants.
在任何给定的语言中,替代评估顺序的选择取决于运算符的优先级和结合性,这些概念我们在第 2.1.3 节中介绍过。优先级和结合性问题不会出现在前缀或后缀表示法中。
In any given language, the choice among alternative evaluation orders depends on the precedence and associativity of operators, concepts we introduced in Section 2.1.3. Issues of precedence and associativity do not arise in prefix or postfix notation.
C 的优先级结构(以及其后代 C++、Java 和 C#,略有不同)比大多数其他语言的优先级结构丰富得多。事实上,它比图 6.1所示的更丰富,因为 C 语言中还有几个额外的构造,包括类型转换、函数调用、数组下标和记录字段选择,都被归类为运算符。可以公平地说,大多数 C 程序员并不记得他们语言的所有优先级。语言设计者的意图大概是确保当不使用括号强制执行特定的求值顺序时通常会发生“正确的事情”。然而,明智的程序员不会依赖这一点,而是查阅手册或添加括号。
The precedence structure of C (and, with minor variations, of its descendants, C++, Java, and C#) is substantially richer than that of most other languages. It is, in fact, richer than shown in Figure 6.1, because several additional constructs, including type casts, function calls, array subscripting, and record field selection, are classified as operators in C. It is probably fair to say that most C programmers do not remember all of their language's precedence levels. The intent of the language designers was presumably to ensure that “the right thing” will usually happen when parentheses are not used to force a particular evaluation order. Rather than count on this, however, the wise programmer will consult the manual or add parentheses.
因为优先级和结合性的规则在不同语言之间差别很大,所以使用多种语言的程序员最好多多使用括号。
Because the rules for precedence and associativity vary so much from one language to another, a programmer who works in several languages is wise to make liberal use of parentheses.
在纯函数式语言中,表达式是程序的构建块,计算完全由表达式求值组成。任何单个表达式对整体计算的影响仅限于该表达式为其周围上下文提供的值。复杂的计算使用递归来生成可能无限数量的值、表达式和上下文。
In a purely functional language, expressions are the building blocks of programs, and computation consists entirely of expression evaluation. The effect of any individual expression on the overall computation is limited to the value that expression provides to its surrounding context. Complex computations employ recursion to generate a potentially unbounded number of values, expressions, and contexts.
相比之下,在命令式语言中,计算通常由对内存中变量值的一系列有序更改组成。赋值提供了进行更改的主要方法。每个赋值都采用一对参数:一个值和一个对应将值放入的变量的引用。
In an imperative language, by contrast, computation typically consists of an ordered series of changes to the values of variables in memory. Assignments provide the principal means by which to make the changes. Each assignment takes a pair of arguments: a value and a reference to a variable into which the value should be placed.
一般而言,如果编程语言结构以除返回用于周围上下文的值之外的任何方式影响后续计算(并最终影响程序输出),则该结构具有副作用。赋值可能是最基本的副作用:虽然赋值的求值有时可能会产生一个值,但我们真正关心的是它改变了变量的值,从而影响了变量出现的任何后续计算的结果。
In general, a programming language construct is said to have a side effect if it influences subsequent computation (and ultimately program output) in any way other than by returning a value for use in the surrounding context. Assignment is perhaps the most fundamental side effect: while the evaluation of an assignment may sometimes yield a value, what we really care about is the fact that it changes the value of a variable, thereby influencing the result of any later computation in which the variable appears.
许多命令式语言区分表达式和语句,前者总是产生值,可能有副作用,也可能没有副作用,后者只是为了副作用而执行,不返回任何有用的值。鉴于赋值的重要性,命令式编程有时被描述为“通过副作用进行计算”。
Many imperative languages distinguish between expressions, which always produce a value, and may or may not have side effects, and statements, which are executed solely for their side effects, and return no useful value. Given the centrality of assignment, imperative programming is sometimes described as “computing by means of side effects.”
另一个极端是纯函数式语言,它没有副作用。因此,这种语言中表达式的值仅取决于表达式求值的引用环境,而不取决于求值发生。如果表达式在某一时刻产生某个值,则保证在任何时刻都会产生相同的值。用更通俗的话来说,纯函数式语言中的表达式被称为引用透明的。
At the opposite extreme, purely functional languages have no side effects. As a result, the value of an expression in such a language depends only on the referencing environment in which the expression is evaluated, not on the time at which the evaluation occurs. If an expression yields a certain value at one point in time, it is guaranteed to yield the same value at any point in time. In fancier terms, expressions in a purely functional language are said to be referentially transparent.
Haskell 和 Miranda 是纯函数式的。许多其他语言都是混合型的:ML 和 Lisp 主要是函数式的,但为需要它的程序员提供赋值功能。C#、Python 和 Ruby 主要是命令式的,但提供了各种特性(一等函数、多态性、函数值和聚合、垃圾收集、无限范围),允许它们以很大的函数式风格使用。我们将在未来的几个章节中回到函数式编程及其所需的特性,包括6.2.2、6.6、7.3、8.5.3、8.6和第11章的全部内容。
Haskell and Miranda are purely functional. Many other languages are mixed: ML and Lisp are mostly functional, but make assignment available to programmers who want it. C#, Python, and Ruby are mostly imperative, but provide a variety of features (first-class functions, polymorphism, functional values and aggregates, garbage collection, unlimited extent) that allow them to be used in a largely functional style. We will return to functional programming, and the features it requires, in several future sections, including 6.2.2, 6.6, 7.3, 8.5.3, 8.6, and all of Chapter 11.
从表面上看,赋值似乎是一个非常简单的操作。然而,在表面之下,不同的命令式语言在赋值的语义上存在一些微妙但重要的差异。这些差异通常是看不见的,因为它们不会影响简单程序的行为。然而,它们对使用指针的程序有重大影响,我们将在第8.5 节中进一步详细探讨。我们在这里对这些问题进行了介绍。
On the surface, assignment appears to be a very straightforward operation. Below the surface, however, there are some subtle but important differences in the semantics of assignment in different imperative languages. These differences are often invisible, because they do not affect the behavior of simple programs. They have a major impact, however, on programs that use pointers, and will be explored in further detail in Section 8.5. We provide an introduction to the issues here.
我们将在第 9.3.1 节中进一步考虑参考文献。
We will consider references further in Section 9.3.1.
在使用引用模型的语言中,每个变量都是左值。当它出现在需要右值的上下文中时,必须取消引用才能获得它引用的值。在大多数具有引用模型的语言(包括 Clu)中,取消引用是隐式和自动的。在 ML 中,程序员必须使用显式取消引用运算符,以前缀感叹号表示。我们将在8.5.1 节中重新讨论 ML 指针。
In a language that uses the reference model, every variable is an l-value. When it appears in a context that expects an r-value, it must be dereferenced to obtain the value to which it refers. In most languages with a reference model (including Clu), the dereference is implicit and automatic. In ML, the programmer must use an explicit dereference operator, denoted with a prefix exclamation point. We will revisit ML pointers in Section 8.5.1.
如果变量引用的值可以“就地”改变(就像在许多具有链接数据结构的程序中那样),或者变量可以引用恰好具有“相同”值的不同对象,那么变量的值模型和引用模型之间的差异就变得尤为重要(具体而言,它会影响程序的输出和行为)。在后一种情况下,区分引用同一对象的变量和引用不同对象的变量(这些对象的值恰好(此刻)相等)就变得很重要。(正如我们将在第 7.4 节和11.3.3节中看到的,Lisp 提供了多种相等概念,以适应这种区别。)我们将在第 8.5 节中进一步讨论变量的值模型和引用模型。
The difference between the value and reference models of variables becomes particularly important (specifically, it can affect program output and behavior) if the values to which variables refer can change “in place,” as they do in many programs with linked data structures, or if it is possible for variables to refer to different objects that happen to have the “same” value. In this latter case it becomes important to distinguish between variables that refer to the same object and variables that refer to different objects whose values happen (at the moment) to be equal. (Lisp, as we shall see in Sections 7.4 and 11.3.3, provides more than one notion of equality, to accommodate this distinction.) We will discuss the value and reference models of variables further in Section 8.5.
Java 对内置类型使用值模型,对用户定义类型(类)使用引用模型。C# 和 Eiffel 允许程序员为每个单独的用户定义类型选择值模型和引用模型。C# 类是引用类型;结构是值类型。
Java uses a value model for built-in types and a reference model for user-defined types (classes). C# and Eiffel allow the programmer to choose between the value and reference models for each individual user-defined type. A C# class is a reference type; a struct is a value type.
一个常见的设计目标是使语言的各种特性尽可能正交。正交性意味着特性可以以任意组合使用,所有组合都有意义,并且给定特性的含义是一致的,无论它与其他特性组合如何。
A common design goal is to make the various features of a language as orthogonal as possible. Orthogonality means that features can be used in any combination, the combinations all make sense, and the meaning of a given feature is consistent, regardless of the other features with which it is combined.
C 采取了一种折中的方法。它区分了语句和表达式,但语句的一种类型是“表达式语句”,它计算表达式的值然后将其丢弃;实际上,这允许表达式出现在大多数其他语言中需要语句的任何上下文中。不幸的是,正如我们在第3.7 节中指出的那样,相反的情况并非如此case:语句通常不能在表达式上下文中使用。C 提供了用于选择和排序的特殊表达式形式。Algol 60 将if…then…else定义为语句和表达式。
C takes an intermediate approach. It distinguishes between statements and expressions, but one of the classes of statement is an “expression statement,” which computes the value of an expression and then throws it away; in effect, this allows an expression to appear in any context that would require a statement in most other languages. Unfortunately, as we noted in Section 3.7, the reverse is not the case: statements cannot in general be used in an expression context. C provides special expression forms for selection and sequencing. Algol 60 defines if… then … else as both a statement and an expression.
尽管 C++ 提供了真正的布尔类型 ( bool ),但它也存在与 C 相同的问题,因为它提供了从数字、指针和枚举类型的自动强制转换。Java 和 C# 通过在布尔上下文中禁止整数来消除此问题。赋值运算符仍然是 =,相等性测试仍然是 ==,但语句if (a = b) … 将生成编译时类型冲突错误,除非a和b都是布尔类型。
Though it provides a true Boolean type (bool), C++ shares the problem of C, because it provides automatic coercions from numeric, pointer, and enumeration types. Java and C# eliminate the problem by disallowing integers in Boolean contexts. The assignment operator is still =, and the equality test is still ==, but the statement if (a = b) … will generate a compile-time type clash error unless a and b are both of Boolean type.
赋值运算符 (+=, −=) 和递增和递减运算符 (++, −−) 在应用于 C 中的指针时(假设这些指针指向一个数组),都会“正确”地执行操作。如果p指向数组的元素i ,其中每个元素占用n 个字节(包括对齐所需的任何字节,如第 C-5.1 节所述),则p += 3指向元素i + 3,即内存中 3n 个字节之后的位置。我们将在第 8.5.1 节中更详细地讨论 C 中的指针和数组。
Both the assignment operators (+=, −=) and the increment and decrement operators (++, −−) do “the right thing” when applied to pointers in C (assuming those pointers point into an array). If p points to element i of an array, where each element occupies n bytes (including any bytes required for alignment, as discussed in Section C-5.1), then p += 3 points to element i + 3, 3n bytes later in memory. We will discuss pointers and arrays in C in more detail in Section 8.5.1.
由于命令式语言已经提供了设置变量值的结构(赋值语句),因此它们并不总是提供在变量声明中指定变量初始值的方法。不过,有以下几个原因可以说明为什么这样的初始值可能很有用:
Because they already provide a construct (the assignment statement) to set the value of a variable, imperative languages do not always provide a means of specifying an initial value for a variable in its declaration. There are several reasons, however, why such initial values may be useful:
1.如图 3.3所示,子程序的局部静态变量需要初始值才能使用。
1. As suggested in Figure 3.3, a static variable that is local to a subroutine needs an initial value in order to be useful.
2. 对于任何静态分配的变量,声明中指定的初始值可以由编译器在全局内存中预先分配,从而避免在运行时分配初始值的成本。
2. For any statically allocated variable, an initial value that is specified in the declaration can be preallocated in global memory by the compiler, avoiding the cost of assigning an initial value at run time.
3. 意外使用未初始化的变量是最常见的编程错误之一。防止此类错误(或至少确保错误行为可重复)的最简单方法之一是在首次声明每个变量时为其赋值。
3. Accidental use of an uninitialized variable is one of the most common programming errors. One of the easiest ways to prevent such errors (or at least ensure that erroneous behavior is repeatable) is to give every variable a value when it is first declared.
大多数语言允许在声明中初始化内置类型的变量。更完整、更正交的初始化方法需要聚合符号:用户定义的复合类型的结构化值。聚合可以在多种语言中找到,包括 C、C++、Ada、Fortran 90 和 ML;我们将在第7.1.3 节中进一步讨论它们。
Most languages allow variables of built-in types to be initialized in their declarations. A more complete and orthogonal approach to initialization requires a notation for aggregates: built-up structured values of user-defined composite types. Aggregates can be found in several languages, including C, C++, Ada, Fortran 90, and ML; we will discuss them further in Section 7.1.3.
需要强调的是,初始化仅对静态分配的变量节省时间。运行时在堆栈或堆中分配的变量必须在运行时初始化。5还值得注意的是,使用未初始化变量的问题不仅发生在细化之后,还可能由于任何破坏变量值而不提供新值的操作而发生。最常见的两种此类操作是通过指针引用的对象的显式释放和变体记录的标签修改。我们将分别在第 8.5 节和C-8.1.3节中进一步讨论这些操作。
It should be emphasized that initialization saves time only for variables that are statically allocated. Variables allocated in the stack or heap at run time must be initialized at run time.5 It is also worth noting that the problem of using an uninitialized variable occurs not only after elaboration, but also as a result of any operation that destroys a variable's value without providing a new one. Two of the most common such operations are explicit deallocation of an object referenced through a pointer and modification of the tag of a variant record. We will consider these operations further in Sections 8.5 and C-8.1.3, respectively.
如果在变量声明中未明确赋予其初始值,则语言可以指定默认值。例如,在 C 语言中,程序员未提供初始值的静态分配变量保证在内存中表示为好像它们已被初始化为零。对于大多数机器上的大多数类型,这都是一串零位,允许语言实现利用大多数操作系统(出于安全原因)用零填充新分配的内存这一事实。零初始化以递归方式应用于用户定义复合类型变量的子组件。Java 和 C# 为所有类类型对象的字段提供类似的保证,而不仅仅是静态分配的字段。大多数脚本语言为所有类型的所有变量都提供默认初始值,无论其范围或生存期如何。
If a variable is not given an initial value explicitly in its declaration, the language may specify a default value. In C, for example, statically allocated variables for which the programmer does not provide an initial value are guaranteed to be represented in memory as if they had been initialized to zero. For most types on most machines, this is a string of zero bits, allowing the language implementation to exploit the fact that most operating systems (for security reasons) fill newly allocated memory with zeros. Zero-initialization applies recursively to the subcomponents of variables of user-defined composite types. Java and C# provide a similar guarantee for the fields of all class-typed objects, not just those that are statically allocated. Most scripting languages provide a default initial value for all variables, of all types, regardless of scope or lifetime.
语言或实现可以选择将未初始化变量的使用定义为动态语义错误,并在运行时捕获这些错误,而不是为每个未初始化变量赋予默认值。语义检查的优点是它们通常可以识别出因默认值的存在而被掩盖或变得更加隐蔽的程序错误。有了适当的硬件支持,未初始化变量检查甚至可以像默认值一样便宜,至少对于某些类型而言。具体来说,依赖于 IEEE 浮点算术标准的编译器可以用信号NaN值填充未初始化的浮点数,如第 C-5.2.2 节所述。任何在计算中使用此类值的尝试都将导致硬件中断,语言实现可能会捕获该中断(在操作系统的帮助下),并使用它来触发语义错误消息。
Instead of giving every uninitialized variable a default value, a language or implementation can choose to define the use of an uninitialized variable as a dynamic semantic error, and can catch these errors at run time. The advantage of the semantic checks is that they will often identify a program bug that is masked or made more subtle by the presence of a default value. With appropriate hardware support, uninitialized variable checks can even be as cheap as default values, at least for certain types. In particular, a compiler that relies on the IEEE standard for floating-point arithmetic can fill uninitialized floating-point numbers with a signaling NaN value, as discussed in Section C-5.2.2. Any attempt to use such a value in a computation will result in a hardware interrupt, which the language implementation may catch (with a little help from the operating system), and use to trigger a semantic error message.
不幸的是,对于大多数机器上的大多数类型,在运行时捕获未初始化变量的所有使用的成本要高得多。如果变量在内存中的表示的每个可能的位模式都指定某个合法值(通常情况如此),则必须在某处分配额外的空间来保存已初始化/未初始化标志。此标志必须在阐述时设置为“未初始化”,在分配时设置为“已初始化”。每次使用时,或者至少在代码改进者无法证明是冗余的每次使用时,也必须检查它(通过额外的代码)。
For most types on most machines, unfortunately, the costs of catching all uses of an uninitialized variable at run time are considerably higher. If every possible bit pattern of the variable's representation in memory designates some legitimate value (and this is often the case), then extra space must be allocated somewhere to hold an initialized/uninitialized flag. This flag must be set to “uninitialized” at elaboration time and to “initialized” at assignment time. It must also be checked (by extra code) at every use, or at least at every use that the code improver is unable to prove is redundant.
许多面向对象语言(其中包括 Java 和 C#)允许程序员定义类型,即使声明中未指定初始值,动态分配的变量的初始化也会自动发生。一些语言(尤其是 C++)还仔细区分了初始化和赋值。初始化被解释为对变量类型的构造函数的调用,初始值作为参数。在没有强制的情况下,赋值被解释为对类型的赋值运算符的调用,或者,如果没有定义,则被解释为对赋值右侧值的简单逐位复制。初始化和赋值之间的区别对于执行自己的存储管理的用户定义的抽象数据类型尤其重要。一个典型的例子是可变长度的字符串。对这样的字符串进行赋值通常必须释放字符串旧值所占用的空间,然后再为新值分配空间。字符串的初始化必须简单地分配空间。使用非平凡值进行初始化通常比默认初始化后再赋值更便宜,因为它避免了释放为默认值分配的空间。我们将在第 10.3.2 节中再次讨论这个问题。
Many object-oriented languages (Java and C# among them) allow the programmer to define types for which initialization of dynamically allocated variables occurs automatically, even when no initial value is specified in the declaration. Some—notably C++—also distinguish carefully between initialization and assignment. Initialization is interpreted as a call to a constructor function for the variable's type, with the initial value as an argument. In the absence of coercion, assignment is interpreted as a call to the type's assignment operator or, if none has been defined, as a simple bit-wise copy of the value on the assignment's right-hand side. The distinction between initialization and assignment is particularly important for user-defined abstract data types that perform their own storage management. A typical example occurs in variable-length character strings. An assignment to such a string must generally deallocate the space consumed by the old value of the string before allocating space for the new value. An initialization of the string must simply allocate space. Initialization with a nontrivial value is generally cheaper than default initialization followed by assignment, because it avoids deallocation of the space allocated for the default value. We will return to this issue in Section 10.3.2.
Java 和 C# 都不区分初始化和赋值:可以在声明中给出初始值,但这与紧接着的赋值相同。Java 对用户定义对象类型的所有变量使用引用模型,并提供自动存储回收,因此赋值永远不会复制值。C# 允许程序员在需要时指定值模型(在这种情况下赋值会复制值),但其他方面与 Java 相似。
Neither Java nor C# distinguishes between initialization and assignment: an initial value can be given in a declaration, but this is the same as an immediate subsequent assignment. Java uses a reference model for all variables of user-defined object types, and provides for automatic storage reclamation, so assignment never copies values. C# allows the programmer to specify a value model when desired (in which case assignment does copy values), but otherwise mirrors Java.
该顺序之所以重要,主要有两个原因:
There are two main reasons why the order can be important:
由于代码改进的重要性,大多数语言手册都说操作数和参数的求值顺序是未定义的。(Java 和 C# 在这方面很不寻常:它们要求从左到右进行求值。)在没有强制顺序的情况下,编译器可以选择任何可能导致更快代码的顺序。
Because of the importance of code improvement, most language manuals say that the order of evaluation of operands and arguments is undefined. (Java and C# are unusual in this regard: they require left-to-right evaluation.) In the absence of an enforced order, the compiler can choose whatever order is likely to result in faster code.
许多语言(包括 Pascal 及其大多数后代)都提供动态语义检查来检测算术溢出。在某些实现中,可以禁用这些检查以消除其运行时开销。在 C 和 C++ 中,算术溢出的影响取决于实现。在 Java 中,它定义明确:语言定义指定所有数字类型的大小,并要求二进制补码整数和 IEEE 浮点运算。在 C# 中,程序员可以通过用 checked 或 unchecked 关键字标记表达式或语句来明确请求检查的存在或不存在。在完全不同的方面,Scheme、Common Lisp 和几种脚本语言对整数的大小没有先验限制;根据需要分配空间来保存超大值。
Many languages, including Pascal and most of its descendants, provide dynamic semantic checks to detect arithmetic overflow. In some implementations these checks can be disabled to eliminate their run-time overhead. In C and C++, the effect of arithmetic overflow is implementation-dependent. In Java, it is well defined: the language definition specifies the size of all numeric types, and requires two's complement integer and IEEE floating-point arithmetic. In C#, the programmer can explicitly request the presence or absence of checks by tagging an expression or statement with the checked or unchecked keyword. In a completely different vein, Scheme, Common Lisp, and several scripting languages place no a priori limit on the size of integers; space is allocated to hold extra-large values on demand.
如果我们将 and 和 or 视为二元运算符,那么短路可以被视为延迟或惰性求值的一个例子:操作数未经求值就“传递”。在内部,运算符在任何情况下都会求值第一个操作数,仅在需要时才求值第二个操作数。在 Algol 68 等允许在表达式中使用任意控制流构造的语言中,可以使用if…then…else明确指定条件求值;参见练习 6.13。
If we think of and and or as binary operators, short circuiting can be considered an example of delayed or lazy evaluation: the operands are “passed” unevaluated. Internally, the operator evaluates the first operand in any case, the second only when needed. In a language like Algol 68, which allows arbitrary control flow constructs to be used inside expressions, conditional evaluation can be specified explicitly with if… then … else; see Exercise 6.13.
当用于确定选择或迭代构造中的控制流时,短路布尔表达式实际上不必计算布尔值;它们只需确保控制在任何给定情况下都采用正确的路径。我们将在第6.4.1 节中更详细地介绍短路表达式的代码生成。
When used to determine the flow of control in a selection or iteration construct, short-circuit Boolean expressions do not really have to calculate a Boolean value; they simply have to ensure that control takes the proper path in any given situation. We will look more closely at the generation of code for short-circuit expressions in Section 6.4.1.
从 20 世纪 60 年代末开始,主要是为了回应 Edsger Dijkstra [ Dij68b ] 的一篇文章,6 位语言设计者就 goto 的优缺点展开了激烈的辩论。可以说,反对者获胜了。Ada 和 C# 仅在有限的上下文中允许 goto。Modula(1、2 和 3)、Clu、Eiffel、Java 和大多数脚本语言根本不允许使用 goto。Fortran 90 和 C++ 允许使用 goto 主要是为了与它们的前身语言兼容。(Java 将标记 goto 保留为关键字,以便当程序员错误地使用 C++ goto 时,Java 编译器更容易生成良好的错误消息。)
Beginning in the late 1960s, largely in response to an article by Edsger Dijkstra [Dij68b],6 language designers hotly debated the merits and evils of gotos. It seems fair to say the detractors won. Ada and C# allow gotos only in limited contexts. Modula (1, 2, and 3), Clu, Eiffel, Java, and most of the scripting languages do not allow them at all. Fortran 90 and C++ allow them primarily for compatibility with their predecessor languages. (Java reserves the token goto as a keyword, to make it easier for a Java compiler to produce good error messages when a programmer uses a C++ goto by mistake.)
放弃 goto 是软件工程领域一场更大的“革命”的一部分,即结构化编程。结构化编程是 20 世纪 70 年代的“热门趋势”,就像面向对象编程是 20 世纪 90 年代的趋势一样。结构化编程强调自上而下的设计(即逐步细化)、代码模块化、结构化类型(记录、集合、指针、多维数组)、描述性变量和常量名称以及广泛的注释约定。结构化编程的开发人员能够证明,在子程序中,几乎任何设计良好的命令式算法都可以仅通过排序、选择和迭代来优雅地表达。结构化语言不使用标签,而是依靠词汇嵌套结构的边界作为分支控制的目标。
The abandonment of gotos was part of a larger “revolution” in software engineering known as structured programming. Structured programming was the “hot trend” of the 1970s, in much the same way that object-oriented programming was the trend of the 1990s. Structured programming emphasizes top-down design (i.e., progressive refinement), modularization of code, structured types (records, sets, pointers, multidimensional arrays), descriptive variable and constant names, and extensive commenting conventions. The developers of structured programming were able to demonstrate that within a subroutine, almost any well-designed imperative algorithm can be elegantly expressed with only sequencing, selection, and iteration. Instead of labels, structured languages rely on the boundaries of lexically nested constructs as the targets of branching control.
Algol 60 开创了许多现代程序员熟悉的结构化控制流构造。其中包括if…then…else构造以及枚举(for)和逻辑(while)控制循环。现代 case (switch)语句由 Wirth 和 Hoare 在 Algol W [ WH66 ]中引入,分别作为 Fortran 和 Algol 60 中非结构化的计算 goto 和 switch 构造的替代方案。(C 的 switch 语句与 Algol W case语句的相似性比与 Algol 60 switch的相似性更高。)
Many of the structured control-flow constructs familiar to modern programmers were pioneered by Algol 60. These include the if… then … else construct and both enumeration (for) and logically (while) controlled loops. The modern case (switch) statement was introduced by Wirth and Hoare in Algol W [WH66] as an alternative to the more unstructured computed goto and switch constructs of Fortran and Algol 60, respectively. (The switch statement of C bears a closer resemblance to the Algol W case statement than to the Algol 60 switch.)
一旦定义了主要的结构化构造,围绕goto的大部分争议就围绕着少数特殊情况,每种情况最终都以结构化的方式得到解决。以前 goto可能用于跳转到当前子程序的末尾,而现在大多数现代语言都提供了显式的return语句。以前goto可能用于退出循环中间,而现在大多数现代语言都提供了break或exit语句来达到此目的。(某些语言还提供了只跳过当前迭代剩余部分的语句:C 中的continue ; Fortran 90 中的cycle ;Perl 中的next。)更重要的是,有几种语言允许程序在单个操作中从嵌套的子程序调用链中返回,许多语言都提供了一种引发异常并传播到周围环境的方法。这两种功能可能都曾尝试使用(非本地)goto来实现。
Once the principal structured constructs had been defined, most of the controversy surrounding gotos revolved around a small number of special cases, each of which was eventually addressed in structured ways. Where once a goto might have been used to jump to the end of the current subroutine, most modern languages provide an explicit return statement. Where once a goto might have been used to escape from the middle of a loop, most modern languages provide a break or exit statement for this purpose. (Some languages also provide a statement that will skip the remainder of the current iteration only: continue in C; cycle in Fortran 90; next in Perl.) More significantly, several languages allow a program to return from a nested chain of subroutine calls in a single operation, and many provide a way to raise an exception that propagates out to some surrounding context. Both of these capabilities might once have been attempted with (nonlocal) gotos.
如果发生非本地goto,语言实现必须保证修复子例程调用信息的运行时堆栈。此修复操作称为展开。它不仅要求实现释放我们已退出的任何子例程的堆栈框架,还要求它执行任何簿记操作,例如恢复寄存器内容,这些操作将在从这些例程返回时执行。
In the event of a nonlocal goto, the language implementation must guarantee to repair the run-time stack of subroutine call information. This repair operation is known as unwinding. It requires not only that the implementation deallocate the stack frames of any subroutines from which we have escaped, but also that it perform any bookkeeping operations, such as restoration of register contents, that would have been performed when returning from those routines.
作为非局部goto的更结构化的替代方案,Common Lisp 提供了一个return-from语句,该语句命名要返回的词汇上周围的函数或块,并且还提供返回值(消除了示例 6.40中对人工rtn变量的需要)。
As a more structured alternative to the nonlocal goto, Common Lisp provides a return-from statement that names the lexically surrounding function or block from which to return, and also supplies a return value (eliminating the need for the artificial rtn variable in Example 6.40).
多级返回的概念假设被调用者知道调用者期望什么,并能返回适当的值。在相关的、可能更常见的情况下,深度嵌套的块或子程序可能会发现它无法继续执行其通常的功能,而且缺少以任何优雅方式恢复所需的上下文信息。Eiffel 通过以下说法正式化了这一概念:每个软件组件都有一个契约——它所执行的功能的规范。无法履行其契约的组件被称为失败。它不能以正常方式返回,而是必须安排控制权“退出”到程序能够恢复的某个上下文。需要程序“退出”的条件通常称为异常。我们在C-2.3.5 节中提到了一个例子,其中我们考虑了从递归下降解析器中的语法错误进行短语级恢复。
The notion of a multilevel return assumes that the callee knows what the caller expects, and can return an appropriate value. In a related and arguably more common situation, a deeply nested block or subroutine may discover that it is unable to proceed with its usual function, and moreover lacks the contextual information it would need to recover in any graceful way. Eiffel formalizes this notion by saying that every software component has a contract—a specification of the function it performs. A component that is unable to fulfill its contract is said to fail. Rather than return in the normal way, it must arrange for control to “back out” to some context in which the program is able to recover. Conditions that require a program to “back out” are usually called exceptions. We mentioned an example in Section C-2.3.5, where we considered phrase-level recovery from syntax errors in a recursive descent parser.
可以使用非本地goto或多级返回来消除辅助布尔值,但是我们返回到的调用者仍然必须明确检查状态代码。作为一种结构化的替代方案,许多现代语言提供了一种异常处理机制,以便从异常中进行方便的非本地恢复。我们将在第 9.4 节中更详细地讨论异常处理。通常,程序员会将一个称为处理程序的代码块附加到可能出现异常的任何计算中。处理程序的工作是采取从异常中恢复所需的任何补救措施。如果受保护的计算以正常方式完成,则跳过处理程序的执行。
The auxiliary Booleans can be eliminated by using a nonlocal goto or multilevel return, but the caller to which we return must still inspect status codes explicitly. As a structured alternative, many modern languages provide an exception-handling mechanism for convenient, nonlocal recovery from exceptions. We will discuss exception handling in more detail in Section 9.4. Typically the programmer appends a block of code called a handler to any computation in which an exception may arise. The job of the handler is to take whatever remedial action is required to recover from the exception. If the protected computation completes in the normal fashion, execution of the handler is skipped.
多级返回和结构化异常具有很强的相似性。两者都涉及从某个内部嵌套上下文返回到外部上下文的控制转移,并在途中展开堆栈。区别在于计算发生的位置。在多级返回中,内部上下文拥有所需的所有信息。它完成计算,在适当的情况下生成返回值,并以不需要后处理的方式转移到外部上下文。相比之下,在异常情况下,内部上下文无法完成其工作 - 它无法履行其合同。它执行“异常”返回,触发处理程序的执行。
Multilevel returns and structured exceptions have strong similarities. Both involve a control transfer from some inner, nested context back to an outer context, unwinding the stack on the way. The distinction lies in where the computing occurs. In a multilevel return the inner context has all the information it needs. It completes its computation, generating a return value if appropriate, and transfers to the outer context in a way that requires no post-processing. At an exception, by contrast, the inner context cannot complete its work—it cannot fulfill its contract. It performs an “abnormal” return, triggering execution of the handler.
Common Lisp 和 Ruby 都提供了多级返回和异常的机制,但这种双重支持相对较少见。大多数语言仅支持异常;程序员通过编写简单的处理程序来实现多级返回。不幸的是,术语过多,Common Lisp 和 Ruby 用于多级返回的名称catch和throw在其他几种语言中用于异常。
Common Lisp and Ruby provide mechanisms for both multilevel returns and exceptions, but this dual support is relatively rare. Most languages support only exceptions; programmers implement multilevel returns by writing a trivial handler. In an unfortunate overloading of terminology, the names catch and throw, which Common Lisp and Ruby use for multilevel returns, are used for exceptions in several other languages.
通过定义所谓的延续,可以概括非局部goto (展开堆栈)的概念。从低级角度来说,延续由代码地址、跳转到该地址时应建立(或恢复)的引用环境以及对另一个延续的引用组成,该引用表示在后续子例程返回时应执行的操作。(返回延续的链构成了运行时堆栈的回溯。)从高级角度来说,延续是一种抽象,它捕获了执行可能继续的上下文。延续是指向性语义的基础。它们还作为一等值出现在几种编程语言(尤其是 Scheme 和 Ruby)中,允许程序员定义新的控制流构造。
The notion of nonlocal gotos that unwind the stack can be generalized by defining what are known as continuations. In low-level terms, a continuation consists of a code address, a referencing environment that should be established (or restored) when jumping to that address, and a reference to another continuation that represents what to do in the event of a subsequent subroutine return. (The chain of return continuations constitutes a backtrace of the run-time stack.) In higher-level terms, a continuation is an abstraction that captures a context in which execution might continue. Continuations are fundamental to denotational semantics. They also appear as first-class values in several programming languages (notably Scheme and Ruby), allowing the programmer to define new control-flow constructs.
Scheme 中的延续支持采用名为call-with-current-continuation的函数形式,通常缩写为call/cc。此函数接受单个参数f,而 f 本身是一个参数的函数。Call /cc调用f ,将延续c作为参数传递,该延续捕获当前程序计数器、引用环境和堆栈回溯。延续以闭包形式实现,与用于表示作为参数传递的子例程的闭包没有区别。在未来的任何时候,f都可以调用c,向其传递一个值v。该调用将“返回” v到c的捕获上下文中,就好像它是由对call/cc的原始调用返回的一样。
Continuation support in Scheme takes the form of a function named call-with-current-continuation, often abbreviated call/cc. This function takes a single argument f, which is itself a function of one argument. Call/cc calls f, passing as argument a continuation c that captures the current program counter, referencing environment, and stack backtrace. The continuation is implemented as a closure, indistinguishable from the closures used to represent subroutines passed as parameters. At any point in the future, f can call c, passing it a value, v. The call will “return” v into c's captured context, as if it had been returned by the original call to call/cc.
Call/cc足以构建各种控制抽象,包括goto、循环中退出、多级返回、异常、迭代器(第 6.5.3 节)、按名称调用参数(第 9.3.1 节)和协同程序(第 9.5 节)。它甚至包含了从子程序返回的概念,尽管在实践中它很少取代它。如果以规范的方式使用,延续性会使语言具有惊人的可扩展性。同时,它们允许不规范的程序员构建完全难以捉摸的程序。
Call/cc suffices to build a wide variety of control abstractions, including gotos, midloop exits, multilevel returns, exceptions, iterators (Section 6.5.3), call-by-name parameters (Section 9.3.1), and coroutines (Section 9.5). It even subsumes the notion of returning from a subroutine, though it seldom replaces it in practice. Used in a disciplined way, continuations make a language surprisingly extensible. At the same time, they allow the undisciplined programmer to construct completely inscrutable programs.
与赋值一样,排序是命令式编程的核心。它是控制副作用(例如赋值)发生顺序的主要手段:当程序文本中一条语句跟在另一条语句后面时,第一条语句先于第二条语句执行。在大多数命令式语言中,语句列表可以用begin…end或{ … }分隔符括起来,然后在任何需要单个语句的上下文中使用。这种分隔列表通常称为复合语句。复合语句(可选地以一组声明开头)有时称为块。
Like assignment, sequencing is central to imperative programming. It is the principal means of controlling the order in which side effects (e.g., assignments) occur: when one statement follows another in the program text, the first statement executes before the second. In most imperative languages, lists of statements can be enclosed with begin… end or { … } delimiters and then used in any context in which a single statement is expected. Such a delimited list is usually called a compound statement. A compound statement optionally preceded by a set of declarations is sometimes called a block.
在 Algol 68 等语言中,语句和表达式之间的区别被模糊或消除,语句(表达式)列表的值是其最后一个元素的值。在 Common Lisp 中,程序员可以选择返回第一个元素、第二个元素或最后一个元素的值。当然,除非不参与返回值的子表达式有副作用,否则排序是无用的操作。Lisp 中的各种排序结构仅用于不符合纯函数式编程模型的程序片段。
In languages like Algol 68, which blur or eliminate the distinction between statements and expressions, the value of a statement (expression) list is the value of its final element. In Common Lisp, the programmer can choose to return the value of the first element, the second, or the last. Of course, sequencing is a useless operation unless the subexpressions that do not play a part in the return value have side effects. The various sequencing constructs in Lisp are used only in program fragments that do not conform to a purely functional programming model.
即使在命令式语言中,某些副作用的价值也存在争议。例如,在欧几里得和图灵中,函数(即返回值,因此可以出现在表达式中的子程序)不允许有副作用。除其他外,副作用自由确保欧几里得或图灵函数(就像数学中的对应函数一样)始终是幂等的:如果使用同一组参数重复调用,它将始终返回相同的值,并且连续调用的次数(第一次之后)不会影响后续执行的结果。此外,函数的副作用自由意味着子表达式的值永远不会取决于该子表达式是在调用其他子表达式中的函数之前还是之后求值。这些属性使程序员或定理证明系统更容易推断程序行为。它们还简化了代码改进,例如通过允许安全地重新排列表达式。
Even in imperative languages, there is debate as to the value of certain kinds of side effects. In Euclid and Turing, for example, functions (i.e., subroutines that return values, and that therefore can appear within expressions) are not permitted to have side effects. Among other things, side-effect freedom ensures that a Euclid or Turing function, like its counterpart in mathematics, is always idempotent: if called repeatedly with the same set of arguments, it will always return the same value, and the number of consecutive calls (after the first) will not affect the results of subsequent execution. In addition, side-effect freedom for functions means that the value of a subexpression will never depend on whether that subexpression is evaluated before or after calling a function in some other subexpression. These properties make it easier for a programmer or theorem-proving system to reason about program behavior. They also simplify code improvement, for example by permitting the safe rearrangement of expressions.
正如我们在第 2.3.2 节中看到的,不同语言在语法细节上有所不同。在 Algol 60 和 Pascal 中,then 子句和else子句都被定义为包含单个语句(当然可以是begin...end复合语句)。为了避免语法歧义,Algol 60 要求 then 后的语句以if以外的其他内容开头(begin也可以)。Pascal 消除了这一限制,转而采用“消歧义规则”,将else与最接近的未匹配的 then 相关联。Algol 68、Fortran 77 和更多现代语言通过允许语句列表跟在then或else 之后来避免歧义,并在构造末尾使用终止关键字。
As we saw in Section 2.3.2, languages differ in the details of the syntax. In Algol 60 and Pascal both the then clause and the else clause were defined to contain a single statement (this could of course be a begin… end compound statement). To avoid grammatical ambiguity, Algol 60 required that the statement after the then begin with something other than if (begin is fine). Pascal eliminated this restriction in favor of a “disambiguating rule” that associated an else with the closest unmatched then. Algol 68, Fortran 77, and more modern languages avoid the ambiguity by allowing a statement list to follow either then or else, with a terminating keyword at the end of the construct.
虽然if…then…else语句中的条件是布尔表达式,但通常不需要求值该表达式以将布尔值传入寄存器。大多数机器都提供用于捕获简单比较的条件分支指令。换句话说,选择语句中布尔表达式的目的不是计算要存储的值,而是使控制分支到各个位置。这一观察使我们能够为适合6.1.5 节的短路求值的表达式生成特别高效的代码(称为跳转代码) 。跳转代码不仅适用于诸如if…then…else 之类的选择语句,也适用于逻辑控制循环;我们将在6.5.5 节中讨论后者。
While the condition in an if… then … else statement is a Boolean expression, there is usually no need for evaluation of that expression to result in a Boolean value in a register. Most machines provide conditional branch instructions that capture simple comparisons. Put another way, the purpose of the Boolean expression in a selection statement is not to compute a value to be stored, but to cause control to branch to various locations. This observation allows us to generate particularly efficient code (called jump code) for expressions that are amenable to the short-circuit evaluation of Section 6.1.5. Jump code is applicable not only to selection statements such as if… then … else, but to logically controlled loops as well; we will consider the latter in Section 6.5.5.
在通常的代码生成过程中,表达式子树根的合成属性会获取一个寄存器的名称,表达式的值将在运行时计算到该寄存器中。然后,周围的上下文在生成使用该表达式的代码时使用此寄存器名称。在跳转代码中,根的继承属性会分别告知它在表达式为真或为假时控制应该分支到的地址。
In the usual process of code generation, a synthesized attribute of the root of an expression subtree acquires the name of a register into which the value of the expression will be computed at run time. The surrounding context then uses this register name when generating code that uses the expression. In jump code, inherited attributes of the root inform it of the addresses to which control should branch if the expression is true or false, respectively.
跳转表速度很快:无论控制表达式的值如何,它都会在恒定时间内开始执行case语句的正确分支。当整个case语句标签集密集且不包含大范围时,它也是空间高效的。但是,如果标签集不密集或包含大值范围,它可能会消耗大量空间。计算分支地址的替代方法包括顺序测试、散列和二分搜索。如果case语句标签的总数很少,则顺序测试(如if…then…else语句)是首选方法。它在O ( n ) 时间内选择一个分支,其中n是标签数。如果标签值集很大,但有许多缺失值且没有大范围,则哈希表很有吸引力。使用适当的哈希函数,它将在O (1) 时间内选择正确的分支。不幸的是,哈希表(如跳转表)需要为控制测试表达式的每个可能值设置一个单独的条目,因此不适合具有大值范围的语句。二分查找可以轻松适应范围。它可以在O (log n ) 时间内选择一个分支。
A jump table is fast: it begins executing the correct arm of the case statement in constant time, regardless of the value of the controlling expression. It is also space efficient when the overall set of case statement labels is dense and does not contain large ranges. It can consume an extraordinarily large amount of space, however, if the set of labels is nondense, or includes large value ranges. Alternative methods to compute the address to which to branch include sequential testing, hashing, and binary search. Sequential testing (as in an if… then … else statement) is the method of choice if the total number of case statement labels is small. It chooses an arm in O(n) time, where n is the number of labels. A hash table is attractive if the set of label values is large, but has many missing values and no large ranges. With an appropriate hash function it will choose the right arm in O(1) time. Unfortunately, a hash table, like a jump table, requires a separate entry for each possible value of the controlling tested expression, making it unsuitable for statements with large value ranges. Binary search can accommodate ranges easily. It chooses an arm in O(log n) time.
为了为所有可能的case语句生成良好的代码,编译器需要准备好使用各种策略。在编译期间,它可以在找到 case 语句的各个分支时为其生成代码,同时构建内部数据结构来描述标签集。一旦它看到了所有分支,它就可以决定生成哪种形式的目标代码。为了简单起见,大多数编译器只采用一些可能的实现。有些使用二分搜索代替散列。有些只生成跳转表;其他只生成跳转表加上顺序测试。如果生成的代码意外地大或慢,不太复杂的编译器的用户可能需要重构他们的case语句。
To generate good code for all possible case statements, a compiler needs to be prepared to use a variety of strategies. During compilation it can generate code for the various arms of the case statement as it finds them, while simultaneously building up an internal data structure to describe the label set. Once it has seen all the arms, it can decide which form of target code to generate. For the sake of simplicity, most compilers employ only some of the possible implementations. Some use binary search in lieu of hashing. Some generate only jump tables; others only that plus sequential testing. Users of less sophisticated compilers may need to restructure their case statements if the generated code turns out to be unexpectedly large or slow.
与if…then…else语句一样, case语句的语法细节因语言而异。不同的语言使用不同的标点符号来分隔标签和分支。更重要的是,语言在是否允许标签范围、是否允许(或要求)默认(others)子句以及在运行时如何处理无法匹配任何标签的值方面存在差异。
As with if… then … else statements, the syntactic details of case statements vary from language to language. Different languages use different punctuation to delimit labels and arms. More significantly, languages differ in whether they permit label ranges, whether they permit (or require) a default (others) clause, and in how they handle a value that fails to match any label at run time.
在某些语言(例如,Modula)中,控制表达式具有未出现在标签列表中的值是一种动态语义错误。 Ada要求标签覆盖控制表达式类型的域中的所有可能值;如果类型具有非常大的值,则必须使用范围或其他子句来实现此覆盖。在某些语言中,特别是 C 和 Fortran 90,测试表达式求值为缺失值并不是错误。相反,当值缺失时,整个构造不起作用。
In some languages (e.g., Modula), it is a dynamic semantic error for the controlling expression to have a value that does not appear in the label lists. Ada requires the labels to cover all possible values in the domain of the controlling expression's type; if the type has a very large number of values, then this coverage must be accomplished using ranges or an others clause. In some languages, notably C and Fortran 90, it is not an error for the tested expression to evaluate to a missing value. Rather, the entire construct has no effect when the value is missing.
C 的case (switch)语句语法(C++ 和 Java 保留了该语法)在几个方面有所不同:
C's syntax for case (switch) statements (retained by C++ and Java) is unusual in several respects:
| switch (… /* 控制表达式 */) { | |
| 情况 1: | 条款_A |
| 休息; | |
| 情况 2: | |
| 案例7: | 条款_B |
| 休息; | |
| 案例 3: | |
| 案例4: | |
| 案例5: | 条款_C |
| 休息; | |
| 案例10: | 条款_D |
| 休息; | |
| 默认: | 条款_E |
| 休息; | |
| } | |
然而,大多数情况下,需要在每个分支的末尾插入一个break — 而编译器愿意默默地接受没有 break 的分支 — 是导致意外且难以诊断的错误的原因。C# 保留了熟悉的 C 语法,包括多个连续标签,但要求每个非空分支都以break、goto、continue或return结尾。
Most of the time, however, the need to insert a break at the end of each arm— and the compiler's willingness to accept arms without breaks, silently—is a recipe for unexpected and difficult-to-diagnose bugs. C# retains the familiar C syntax, including multiple consecutive labels, but requires every nonempty arm to end with a break, goto, continue, or return.
迭代和递归是允许计算机重复执行类似操作的两种机制。如果没有其中至少一种机制,程序的运行时间(以及它可以完成的工作量和它可以使用的空间量)将是程序文本大小的线性函数。从非常实际的意义上讲,迭代和递归使计算机不仅仅适用于固定大小的任务。在本节中,我们将重点介绍迭代。递归是第6.6 节的主题。
Iteration and recursion are the two mechanisms that allow a computer to perform similar operations repeatedly. Without at least one of these mechanisms, the running time of a program (and hence the amount of work it can do and the amount of space it can use) would be a linear function of the size of the program text. In a very real sense, it is iteration and recursion that make computers useful for more than fixed-size tasks. In this section we focus on iteration. Recursion is the subject of Section 6.6.
命令式语言的程序员倾向于更多地使用迭代而不是递归(递归在函数式语言中更常见)。在大多数语言中,迭代采用循环的形式。与序列中的语句一样,循环的迭代通常是为了它们的副作用而执行的:它们对变量的修改。循环有两种主要类型,它们在用于确定迭代次数的机制上有所不同。枚举控制的循环对给定有限集中的每个值执行一次;在第一次迭代开始之前就知道迭代次数。逻辑控制的循环执行直到某个布尔条件(通常必须依赖于循环中改变的值)改变值。大多数(但不是全部)语言为这两种循环类型提供了单独的构造。
Programmers in imperative languages tend to use iteration more than they use recursion (recursion is more common in functional languages). In most languages, iteration takes the form of loops. Like the statements in a sequence, the iterations of a loop are generally executed for their side effects: their modifications of variables. Loops come in two principal varieties, which differ in the mechanisms used to determine how many times to iterate. An enumeration-controlled loop is executed once for every value in a given finite set; the number of iterations is known before the first iteration begins. A logically controlled loop is executed until some Boolean condition (which must generally depend on values altered in the loop) changes value. Most (though not all) languages provide separate constructs for these two varieties of loop.
效仿 Clu,许多现代语言允许枚举控制循环对更通用的有限集进行迭代 — — 例如树的节点或集合的元素。我们将在第 6.5.3 节中讨论这些更通用的迭代器。目前我们重点关注算术序列。为简单起见,我们使用“ for 循环”这个名称作为通用术语,即使对于使用不同关键字的语言也是如此。
Following the lead of Clu, many modern languages allow enumeration-controlled loops to iterate over much more general finite sets—the nodes of a tree, for example, or the elements of a collection. We consider these more general iterators in Section 6.5.3. For the moment we focus on arithmetic sequences. For the sake of simplicity, we use the name “for loop“ as a general term, even for languages that use a different keyword.
请注意,这两种翻译都采用了从根本上讲具有方向性的循环结束测试:如图所示,它们假设i的所有实现值都小于last。如果循环“朝另一个方向”进行(即,如果first > last,且step < 0),那么我们将需要使用逆测试来结束循环。为了让编译器做出正确的选择,许多语言都会限制其算术序列的通用性。通常,step需要是一个编译时常量。Ada 实际上将选择限制为 ±1。包括 Ada 和 Pascal 在内的几种语言都要求对“向后”迭代的循环使用特殊的语法(在 Ada 中为 for i in reverse 10..1 ;在 Pascal 中为 for i := 10 downto 1)。
Note that both of these translations employ a loop-ending test that is fundamentally directional: as shown, they assume that all the realized values of i will be smaller than last. If the loop goes “the other direction”—that is, if first > last, and step < 0—then we will need to use the inverse test to end the loop. To allow the compiler to make the right choice, many languages restrict the generality of their arithmetic sequences. Commonly, step is required to be a compile-time constant. Ada actually limits the choices to ±1. Several languages, including both Ada and Pascal, require special syntax for loops that iterate “backward” (for i in reverse 10..1 in Ada; for i := 10 downto 1 in Pascal).
一些处理器(包括 Power 系列、PA-RISC 和大多数 CISC 机器)可以减少迭代次数、将其与零进行比较以及条件分支,所有这些都在一条指令中完成。对于许多循环,这会产生非常高效的代码。
Some processors, including the Power family, PA-RISC, and most CISC machines, can decrement the iteration count, test it against zero, and conditionally branch, all in a single instruction. For many loops this results in very efficient code.
敏锐的读者可能已经注意到,使用迭代计数从根本上取决于在循环开始执行之前能够预测迭代次数。虽然这种预测在包括 Fortran 和 Ada 在内的许多语言中都是可能的,但在其他语言中却不可能,尤其是 C 及其后代。这种区别主要源于以下问题:for 循环构造仅用于迭代,还是仅仅为了使枚举变得容易?如果语言坚持枚举,那么迭代计数就可以了。如果枚举只是循环的一个可能目的 - 更具体地说,如果迭代次数或索引值序列可能会因执行前几次迭代而发生变化 - 那么我们可能需要使用更通用的实现,类似于示例6.59,如果需要,可以进行修改以处理终止测试方向的动态发现。
The astute reader may have noticed that use of an iteration count is fundamentally dependent on being able to predict the number of iterations before the loop begins to execute. While this prediction is possible in many languages, including Fortran and Ada, it is not possible in others, notably C and its descendants. The difference stems largely from the following question: is the for loop construct only for iteration, or is it simply meant to make enumeration easy? If the language insists on enumeration, then an iteration count works fine. If enumeration is only one possible purpose for the loop—more specifically, if the number of iterations or the sequence of index values may change as a result of executing the first few iterations—then we may need to use a more general implementation, along the lines of Example 6.59, modified if necessary to handle dynamic discovery of the direction of the terminating test.
在要求枚举和(仅仅)允许枚举之间的选择体现在几个具体问题上:
The choice between requiring and (merely) enabling enumeration manifests itself in several specific questions:
1. 除了通过枚举机制之外,还能通过其他任何方式控制进入或离开循环吗?
1. Can control enter or leave the loop in any way other than through the enumeration mechanism?
2. 如果循环体修改了用于计算循环结束界限的变量,会发生什么情况?
2. What happens if the loop body modifies variables that were used to compute the end-of-loop bound?
3. 如果循环体修改了索引变量本身会发生什么情况?
3. What happens if the loop body modifies the index variable itself?
4. 循环结束后程序是否可以读取索引变量,如果可以,那么它的值是什么?
4. Can the program read the index variable after the loop has completed, and if so, what will its value be?
问题 (1) 和 (2) 相对容易解决。大多数语言允许break/exit语句提前退出for 循环。Fortran IV 允许goto跳转到循环,但这通常被视为语言缺陷;Fortran 77 和大多数其他语言禁止此类跳转。同样,大多数语言(但不包括 C;参见第 6.5.2 节)规定在第一次迭代之前只计算一次边界,并将其保存在临时位置。对用于计算边界的变量的后续更改不会影响循环迭代的次数。
Questions (1) and (2) are relatively easy to resolve. Most languages allow a break/exit statement to leave a for loop early. Fortran IV allowed a goto to jump into a loop, but this was generally regarded as a language flaw; Fortran 77 and most other languages prohibit such jumps. Similarly, most languages (but not C; see Section 6.5.2) specify that the bound is computed only once, before the first iteration, and kept in a temporary location. Subsequent changes to variables used to compute the bound have no effect on how many times the loop iterates.
Algol W 和 Algol 68 率先提出了一种解决索引修改问题和循环后值问题的有吸引力的解决方案,随后被 Ada、Modula 3 和许多其他语言采用。在这些语言中,循环的头部被认为包含索引的声明。其类型是从循环的边界推断出来的,其范围是循环的主体。由于索引在循环外部不可见,因此其值不是问题。当然,程序员不能将索引命名为与循环内必须访问的任何变量相同的名称,但这是一个严格的本地问题:它不会对循环外产生任何影响。
An attractive solution to both the index modification problem and the post-loop value problem was pioneered by Algol W and Algol 68, and subsequently adopted by Ada, Modula 3, and many other languages. In these, the header of the loop is considered to contain a declaration of the index. Its type is inferred from the bounds of the loop, and its scope is the loop's body. Because the index is not visible outside the loop, its value is not an issue. Of course, the programmer must not give the index the same name as any variable that must be accessed within the loop, but this is a strictly local issue: it has no ramifications outside the loop.
Algol 60 提供了一个单循环结构,该结构包含了更现代的枚举和逻辑控制循环的属性。它允许程序员指定任意数量的“枚举器”,每个枚举器可以是单个值、类似于现代枚举控制循环的值范围或具有终止条件的表达式。Common Lisp 提供了更强大的功能,具有四组独立的子句,用于初始化索引变量(其中可能有任意数量)、测试循环终止(以多种方式中的任何一种)、评估主体表达式以及在循环终止时进行清理。
Algol 60 provided a single loop construct that subsumed the properties of more modern enumeration and logically controlled loops. It allowed the programmer to specify an arbitrary number of “enumerators,” each of which could be a single value, a range of values similar to those of modern enumeration-controlled loops, or an expression with a terminating condition. Common Lisp provides an even more powerful facility, with four separate sets of clauses, to initialize index variables (of which there may be an arbitrary number), test for loop termination (in any of several ways), evaluate body expressions, and clean up at loop termination.
此定义意味着程序员有责任担心溢出对终止条件测试的影响。这还意味着索引和终止条件中包含的任何变量都可以由循环体或其调用的子例程修改,并且这些更改将影响循环控制。这也是程序员的责任。
This definition means that it is the programmer's responsibility to worry about the effect of overflow on testing of the terminating condition. It also means that both the index and any variables contained in the terminating condition can be modified by the body of the loop, or by subroutines it calls, and these changes will affect the loop control. This, too, is the programmer's responsibility.
for 循环头中的三个子句中的任何一个都可以为空(如果缺失,则条件被视为真)。或者,子句可以由逗号分隔的表达式序列组成。C for循环相对于其 while 循环等效项的优势在于紧凑性和清晰度。特别是,影响控制流位于头部内。在while循环中,必须读取循环顶部和底部才能知道发生了什么。
Any of the three clauses in the for loop header can be null (the condition is considered true if missing). Alternatively, a clause can consist of a sequence of comma-separated expressions. The advantage of the C for loop over its while loop equivalent is compactness and clarity. In particular, all of the code affecting the flow of control is localized within the header. In the while loop, one must read both the top and the bottom of the loop to know what is going on.
在我们迄今为止看到的所有示例中(可能除了 Algol 60、Common Lisp 或 C 的组合循环),for循环都会迭代算术序列的元素。但是,一般来说,我们可能希望迭代任何明确定义的集合(在面向对象代码中通常称为集合或容器类的实例)的元素。Clu 引入了一种优雅的迭代器机制(在 Python、Ruby 和 C# 中也有)来精确地做到这一点。Euclid 和几种较新的语言(尤其是 C++、Java 和 Ada 2012)为迭代器对象(有时称为枚举器)定义了一个标准接口,这些接口同样易于使用,但编写起来不那么容易。相反,Icon 提供了迭代器的泛化,称为生成器,它将枚举与回溯搜索相结合。7
In all of the examples we have seen so far (with the possible exception of the combination loops of Algol 60, Common Lisp, or C), a for loop iterates over the elements of an arithmetic sequence. In general, however, we may wish to iterate over the elements of any well-defined set (what are often called collections, or instances of a container class, in object-oriented code). Clu introduced an elegant iterator mechanism (also found in Python, Ruby, and C#) to do precisely that. Euclid and several more recent languages, notably C++, Java, and Ada 2012, define a standard interface for iterator objects (sometimes called enumerators) that are equally easy to use, but not as easy to write. Icon, conversely, provides a generalization of iterators, known as generators, that combines enumeration with backtracking search.7
调用迭代器时,它会计算循环的第一个索引值,并通过执行yield语句将其返回给主程序。yield的行为类似于return,不同之处在于,当完成循环的第一次迭代后将控制权转回迭代器时,迭代器会从上次停止的地方继续执行,而不是从代码的开头继续执行。当迭代器没有更多元素可产生时,它只会返回(没有值),从而终止循环。
When called, the iterator calculates the first index value of the loop, which it returns to the main program by executing a yield statement. The yield behaves like return, except that when control transfers back to the iterator after completion of the first iteration of the loop, the iterator continues where it last left off—not at the beginning of its code. When the iterator has no more elements to yield it simply returns (without a value), thereby terminating the loop.
实际上,迭代器是一个单独的控制线程,具有自己的程序计数器,其执行与它提供索引值的for循环的执行交错。8迭代机制用于将枚举元素所需的算法与使用这些元素的代码“分离”。
In effect, an iterator is a separate thread of control, with its own program counter, whose execution is interleaved with that of the for loop to which it supplies index values.8 The iteration mechanism serves to “decouple” the algorithm required to enumerate elements from the code that uses those elements.
正如在大多数命令式语言中实现的那样,迭代既涉及一种特殊形式的for循环,又涉及一种枚举循环值的机制。这些概念可以分开。Euclid、C++、Java 和 Ada 2012 都提供了枚举控制的循环,让人想起 Python 的循环。但是,它们没有 Yield 语句,也没有单独的线程式上下文来枚举值;相反,迭代器是一个普通对象(在面向对象的意义上),它提供初始化、生成下一个索引值和测试完成的方法。在调用之间,迭代器的状态必须保存在对象的数据成员中。
As realized in most imperative languages, iteration involves both a special form of for loop and a mechanism to enumerate values for the loop. These concepts can be separated. Euclid, C++, Java, and Ada 2012 all provide enumeration-controlled loops reminiscent of those of Python. They have no yield statement, however, and no separate thread-like context to enumerate values; rather, an iterator is an ordinary object (in the object-oriented sense of the word) that provides methods for initialization, generation of the next index value, and testing for completion. Between calls, the state of the iterator must be kept in the object's data members.
由于运算符重载、变量的值模型(需要显式引用和指针)以及缺少垃圾收集,实现 C++ 树迭代器的代码比图 6.6的 Java 版本稍微混乱一些。我们将细节留给练习 6.19。
Code to implement our C++ tree iterator is somewhat messier than the Java version of Figure 6.6, due to operator overloading, the value model of variables (which requires explicit references and pointers), and the lack of garbage collection. We leave the details to Exercise 6.19.
Icon 概括了迭代器的概念,提供了一种生成器机制,使得嵌入的任何表达式能够根据需要枚举多个值。
Icon generalizes the concept of iterators, providing a generator mechanism that causes any expression in which it is embedded to enumerate multiple values on demand.
更深入地
IN MORE DEPTH
我们在配套网站上更详细地讨论了 Icon 生成器。该语言的枚举控制循环,即 every 循环,不仅可以包含生成器,而是任何包含生成器的表达式。生成器还可以用于if语句之类的结构中,如果任何生成的值使条件为真,它将执行其嵌套代码,自动搜索所有可能性。当生成器嵌套时,Icon 会探索生成值的所有可能组合,甚至会在必要时回溯以撤消不成功的控制流分支或分配。
We consider Icon generators in more detail on the companion site. The language's enumeration-controlled loop, the every loop, can contain not only a generator, but any expression that contains a generator. Generators can also be used in constructs like if statements, which will execute their nested code if any generated value makes the condition true, automatically searching through all the possibilities. When generators are nested, Icon explores all possible combinations of generated values, and will even backtrack where necessary to undo unsuccessful control-flow branches or assignments.
Java 以类似的方式扩展了 C/C++ break 语句,并在循环上带有可选标签。
Java extends the C/C++ break statement in a similar fashion, with optional labels on loops.
与迄今为止讨论的控制流机制不同,递归不需要特殊语法。在任何提供子程序(特别是函数)的语言中,所需的只是允许函数调用自身,或调用其他函数,然后依次调用它们。大多数程序员在数据结构类中了解到,递归和(逻辑控制的)迭代提供了同样强大的计算函数方法:任何迭代算法都可以自动重写为递归算法,反之亦然。我们将在下面的第一小节中更详细地比较迭代和递归。在下一小节中,我们将考虑将未求值的表达式传递给函数的可能性。虽然由于实现成本,通常不建议这样做,但这种技术有时可以让我们为仅在可能输入的子集上定义的函数或探索逻辑上无限的数据结构的函数编写优雅的代码。
Unlike the control-flow mechanisms discussed so far, recursion requires no special syntax. In any language that provides subroutines (particularly functions), all that is required is to permit functions to call themselves, or to call other functions that then call them back in turn. Most programmers learn in a data structures class that recursion and (logically controlled) iteration provide equally powerful means of computing functions: any iterative algorithm can be rewritten, automatically, as a recursive algorithm, and vice versa. We will compare iteration and recursion in more detail in the first subsection below. In the following subsection we will consider the possibility of passing unevaluated expressions into a function. While usually inadvisable, due to implementation cost, this technique will sometimes allow us to write elegant code for functions that are only defined on a subset of the possible inputs, or that explore logically infinite data structures.
即使对于非尾递归函数,自动的、通常简单的转换也会产生尾递归代码。转换的一般情况是采用所谓的延续传递风格[ FWH01,第7-8章]。实际上,递归函数总是可以在从递归调用返回后避免执行任何工作,方法是将该工作以延续的形式传递到递归调用中。
Even for functions that are not tail-recursive, automatic, often simple transformations can produce tail-recursive code. The general case of the transformation employs conversion to what is known as continuation-passing style [FWH01, Chaps. 7–8]. In effect, a recursive function can always avoid doing any work after returning from a recursive call by passing that work into the recursive call, in the form of a continuation.
到目前为止的讨论中,我们都隐含地假设参数在传递给子程序之前会进行求值。但事实并非如此。可以将未求值的参数表示传递给子程序,并且仅在实际需要该值时才对其进行求值。前一种选择(在调用之前求值)称为应用顺序求值;后一种选择(仅在实际需要该值时求值)称为正常顺序求值。正常顺序求值是宏中自然出现的情况(第 3.7 节)。它也出现在短路布尔求值(第 6.1.5 节)、按名称调用参数(将在第 9.3.1 节中讨论)和某些函数式语言(将在第 11.5 节中讨论)中。
Throughout the discussion so far we have assumed implicitly that arguments are evaluated before passing them to a subroutine. This need not be the case. It is possible to pass a representation of the unevaluated arguments to the subroutine instead, and to evaluate them only when (if) the value is actually needed. The former option (evaluating before the call) is known as applicative-order evaluation; the latter (evaluating only when the value is actually needed) is known as normal-order evaluation. Normal-order evaluation is what naturally occurs in macros (Section 3.7). It also occurs in short-circuit Boolean evaluation (Section 6.1.5), call-by-name parameters (to be discussed in Section 9.3.1), and certain functional languages (to be discussed in Section 11.5).
Algol 60 默认对用户定义函数使用正常顺序求值(应用顺序也可用)。这种选择大概是为了模仿宏的行为(第 3.7 节)。1960 年的大多数程序员主要用汇编语言编写,并且习惯于使用宏功能。由于 Algol 60 的参数传递机制是语言的一部分,而不是文本缩写,因此不会出现优先级误解或命名冲突等问题。然而,副作用仍然是一个大问题。我们将在第9.3.1 节中更详细地讨论 Algol 60 参数。
Algol 60 uses normal-order evaluation by default for user-defined functions (applicative order is also available). This choice was presumably made to mimic the behavior of macros (Section 3.7). Most programmers in 1960 wrote mainly in assembler, and were accustomed to macro facilities. Because the parameter-passing mechanisms of Algol 60 are part of the language, rather than textual abbreviations, problems like misinterpreted precedence or naming conflicts do not arise. Side effects, however, are still very much an issue. We will discuss Algol 60 parameters in more detail in Section 9.3.1.
从清晰度和效率的角度来看,应用顺序求值通常比正常顺序求值更可取。因此,在大多数语言中采用它是很自然的。然而,在某些情况下,正常顺序求值实际上可以产生更快的代码,或者在应用顺序求值会导致运行时错误时产生有效的代码。在这两种情况下,重要的是,如果参数的值实际上从未需要,那么正常顺序求值有时根本不会求值参数。Scheme 提供了可选的正常顺序以名为delay和force的内置函数形式进行求值。10这些函数提供了惰性求值的实现。在没有副作用的情况下,惰性求值具有与正常顺序求值相同的语义,但实现会跟踪哪些表达式已被求值,因此如果在给定的引用环境中需要多次使用这些值,它可以重用它们的值。
From the points of view of clarity and efficiency, applicative-order evaluation is generally preferable to normal-order evaluation. It is therefore natural for it to be employed in most languages. In some circumstances, however, normal-order evaluation can actually lead to faster code, or to code that works when applicative-order evaluation would lead to a run-time error. In both cases, what matters is that normal-order evaluation will sometimes not evaluate an argument at all, if its value is never actually needed. Scheme provides for optional normal-order evaluation in the form of built-in functions called delay and force.10 These functions provide an implementation of lazy evaluation. In the absence of side effects, lazy evaluation has the same semantics as normal-order evaluation, but the implementation keeps track of which expressions have already been evaluated, so it can reuse their values if they are needed more than once in a given referencing environment.
延迟表达式有时称为承诺。用于跟踪哪些承诺已被评估的机制有时称为记忆化。11由于应用顺序评估是 Scheme 中的默认设置,因此程序员必须使用特殊语法不仅要传递未评估的参数,还要使用它。在 Algol 60 中,子程序头指示要以何种方式传递哪些参数;无论哪种情况,调用点和子程序中的参数使用看起来都相同。
A delayed expression is sometimes called a promise. The mechanism used to keep track of which promises have already been evaluated is sometimes called memoization.11 Because applicative-order evaluation is the default in Scheme, the programmer must use special syntax not only to pass an unevaluated argument, but also to use it. In Algol 60, subroutine headers indicate which arguments are to be passed which way; the point of call and the uses of parameters within subroutines look the same in either case.
我们的最后一类控制流是非确定性。非确定性构造是指在替代方案之间(即在控制路径之间)的选择是故意不指定。我们已经看到了表达式求值中的不确定性示例(第 6.1.4 节):在大多数语言中,运算符或子程序参数可以按任何顺序求值。有些语言,尤其是 Algol 68 和各种并发语言,提供了更广泛的非确定性机制,这些机制也涵盖了语句。
Our final category of control flow is nondeterminacy. A nondeterministic construct is one in which the choice between alternatives (i.e., between control paths) is deliberately unspecified. We have already seen examples of nondeterminacy in the evaluation of expressions (Section 6.1.4): in most languages, operator or subroutine arguments may be evaluated in any order. Some languages, notably Algol 68 and various concurrent languages, provide more extensive nondeterministic mechanisms, which cover statements as well.
更深入地
IN MORE DEPTH
有关非确定性的进一步讨论可以在配套站点上找到。如果没有非确定性构造,则代码片段的作者必须选择某种任意(人为)顺序,其中顺序无关紧要。这样的选择会使构造正式的正确性证明变得更加困难。一些语言设计者还认为这是不优雅的。非确定性最引人注目的用途出现在并发程序中,其中对线程与其对等线程交互的顺序施加任意选择可能会导致整个系统死锁。对于此类程序,可能需要确保非确定性替代方案之间的选择在某种正式意义上是公平的。
Further discussion of nondeterminism can be found on the companion site. Absent a nondeterministic construct, the author of a code fragment in which order does not matter must choose some arbitrary (artificial) order. Such a choice can make it more difficult to construct a formal correctness proof. Some language designers have also argued that it is inelegant. The most compelling uses for nondeterminacy arise in concurrent programs, where imposing an arbitrary choice on the order in which a thread interacts with its peers may cause the system as a whole to deadlock. For such programs one may need to ensure that the choice among nondeterministic alternatives is fair in some formal sense.
在本章中,我们介绍了编程语言中控制流的主要形式:排序、选择、迭代、过程抽象、递归、并发、异常处理和推测以及不确定性。排序指定某些操作要按顺序一个接一个地发生。选择表示在两个或多个控制流选项中进行选择。迭代和递归是重复执行操作的两种方式。递归根据其自身的更简单实例来定义操作;它依赖于过程抽象。迭代重复操作以产生副作用。顺序和迭代是命令式编程的基础。递归是函数式编程的基础。不确定性允许程序员故意不指定控制流的某些方面。我们只是简要地谈到了并发性;这将是第13 章的主题。过程抽象(子程序)是第 9 章的主题。异常处理和推测将在第 9.4 节和第 13.4.4节中介绍。
In this chapter we introduced the principal forms of control flow found in programming languages: sequencing, selection, iteration, procedural abstraction, recursion, concurrency, exception handling and speculation, and nondeterminacy. Sequencing specifies that certain operations are to occur in order, one after the other. Selection expresses a choice among two or more control-flow alternatives. Iteration and recursion are the two ways to execute operations repeatedly. Recursion defines an operation in terms of simpler instances of itself; it depends on procedural abstraction. Iteration repeats an operation for its side effect(s). Sequencing and iteration are fundamental to imperative programming. Recursion is fundamental to functional programming. Nondeterminacy allows the programmer to leave certain aspects of control flow deliberately unspecified. We touched on concurrency only briefly; it will be the subject of Chapter 13. Procedural abstractions (subroutines) are the subject of Chapter 9. Exception handling and speculation will be covered in Sections 9.4 and 13.4.4.
在讨论控制流机制之前,我们先讨论了表达式求值。我们考虑了左值和右值之间的区别,以及变量的值模型(其中变量是数据的命名容器)和变量的引用模型(其中变量是对数据对象的引用)之间的区别。我们考虑了表达式中的优先级、结合性和顺序问题。我们研究了短路布尔求值及其通过跳转代码的实现,这既是影响表达式正确性的语义问题(其子部分并不总是定义明确),也是影响评估复杂布尔表达式所需时间的实现问题。
Our survey of control-flow mechanisms was preceded by a discussion of expression evaluation. We considered the distinction between l-values and r-values, and between the value model of variables, in which a variable is a named container for data, and the reference model of variables, in which a variable is a reference to a data object. We considered issues of precedence, associativity, and ordering within expressions. We examined short-circuit Boolean evaluation and its implementation via jump code, both as a semantic issue that affects the correctness of expressions whose subparts are not always well defined, and as an implementation issue that affects the time required to evaluate complex Boolean expressions.
在我们的调查中,我们遇到过许多控制流构造的例子,它们的语法和语义随着时间的推移已经有了很大的发展。一个重要的早期例子是基于goto的控制流的逐步淘汰和对结构化替代方案的共识的出现。虽然便利性和可读性很难量化,但大多数程序员都同意,像 Ada 这样的语言的控制流构造比 Fortran IV 等语言的控制流构造有了显著的改进。Ada 中专门为纠正早期语言中的控制流问题而设计的功能示例包括结构化构造的显式终止符( end if、end loop等); elsif子句; case语句中的标签范围和默认(others)子句;将for 循环索引隐式声明为只读局部变量;显式return语句;多级循环退出语句;以及异常。
In our survey we encountered many examples of control-flow constructs whose syntax and semantics have evolved considerably over time. An important early example was the phasing out of goto-based control flow and the emergence of a consensus on structured alternatives. While convenience and readability are difficult to quantify, most programmers would agree that the control-flow constructs of a language like Ada are a dramatic improvement over those of, say, Fortran IV. Examples of features in Ada that are specifically designed to rectify control-flow problems in earlier languages include explicit terminators (end if, end loop, etc.) for structured constructs; elsif clauses; label ranges and default (others) clauses in case statements; implicit declaration of for loop indices as read-only local variables; explicit return statements; multilevel loop exit statements; and exceptions.
构造的演进受到许多目标的驱动,包括编程的简易性、语义的优雅性、实现的简易性和运行时效率。在某些情况下,这些目标已被证明是互补的。例如,我们已经看到短路求值既可以加快代码速度,又可以(在许多情况下)使语义更清晰。类似地,为枚举控制循环的索引变量引入新的局部作用域既可以避免循环后索引值的语义问题,也可以(在一定程度上)避免潜在溢出的实现问题。
The evolution of constructs has been driven by many goals, including ease of programming, semantic elegance, ease of implementation, and run-time efficiency. In some cases these goals have proved complementary. We have seen for example that short-circuit evaluation leads both to faster code and (in many cases) to cleaner semantics. In a similar vein, the introduction of a new local scope for the index variable of an enumeration-controlled loop avoids both the semantic problem of the value of the index after the loop and (to some extent) the implementation problem of potential overflow.
在其他情况下,语言语义的改进被认为值得以运行时效率为代价。我们在迭代器的开发中看到了这一点:与许多形式的抽象一样,它们在许多情况下增加了适度的运行时成本(例如,与在循环的控制流中明确嵌入枚举集合的实现相比),但在模块化、清晰度和代码重用机会方面却获得了巨大的回报。同样,Java 的开发人员会认为,对于许多应用程序来说,广泛的语义检查、标准格式的数字类型等提供的可移植性和安全性远比速度重要。
In other cases improvements in language semantics have been considered worth a small cost in run-time efficiency. We saw this in the development of iterators: like many forms of abstraction, they add a modest amount of run-time cost in many cases (e.g., in comparison to explicitly embedding the implementation of the enumerated collection in the control flow of the loop), but with a large pay-back in modularity, clarity, and opportunities for code reuse. In a similar vein, the developers of Java would argue that for many applications the portability and safety provided by extensive semantic checking, standard-format numeric types, and so on are far more important than speed.
在一些情况下,编译器技术的进步或设计人员愿意构建更复杂的编译器,使得合并曾经被认为过于昂贵的功能成为可能。Ada案例语句中的标签范围要求编译器准备好使用二分搜索来生成代码。C++ 中的内联函数消除了在小函数的低效率和宏的混乱语义之间做出选择的需要。异常(我们将在第 9.4.3 节中看到)可以以这样一种方式实现,即它们在常见情况下(当它们不发生时)不产生任何成本,但实现起来相当棘手。迭代器、装箱、泛型(第 7.3.1 节)和一等函数同样相当棘手,但在主流命令式语言中越来越多地出现。
In several cases, advances in compiler technology or in the simple willingness of designers to build more complex compilers have made it possible to incorporate features once considered too expensive. Label ranges in Ada case statements require that the compiler be prepared to generate code employing binary search. In-line functions in C++ eliminate the need to choose between the inefficiency of tiny functions and the messy semantics of macros. Exceptions (as we shall see in Section 9.4.3) can be implemented in such a way that they incur no cost in the common case (when they do not occur), but the implementation is quite tricky. Iterators, boxing, generics (Section 7.3.1), and first-class functions are likewise rather tricky, but are increasingly found in mainstream imperative languages.
某些实现技术(例如,重新排列表达式以发现公共子表达式,或在找到可接受的选择后避免在非确定性构造中评估保护)非常重要,足以证明程序员的负担适度(例如,在必要时添加括号以避免溢出或确保数字稳定性,或确保保护中的表达式没有副作用)。其他语义上有用的机制(例如,惰性求值、延续或真正随机的不确定性)通常被认为足够复杂或昂贵,只有在特殊情况下才值得(如果有的话)。
Some implementation techniques (e.g., rearranging expressions to uncover common subexpressions, or avoiding the evaluation of guards in a nondeterministic construct once an acceptable choice has been found) are sufficiently important to justify a modest burden on the programmer (e.g., adding parentheses where necessary to avoid overflow or ensure numeric stability, or ensuring that expressions in guards are side-effect-free). Other semantically useful mechanisms (e.g., lazy evaluation, continuations, or truly random nondeterminacy) are usually considered complex or expensive enough to be worthwhile only in special circumstances (if at all).
在相对原始的语言中,我们通常可以通过编程约定获得一些缺失功能的好处。例如,在 Fortran 的早期方言中,我们可以将goto的使用限制为模仿更现代语言的控制流的模式。在没有短路求值的语言中,我们可以编写嵌套的选择语句。在没有迭代器的语言中,我们可以编写提供等效功能的子例程集。
In comparatively primitive languages, we can often obtain some of the benefits of missing features through programming conventions. In early dialects of Fortran, for example, we can limit the use of gotos to patterns that mimic the control flow of more modern languages. In languages without short-circuit evaluation, we can write nested selection statements. In languages without iterators, we can write sets of subroutines that provide equivalent functionality.
6.1我们在 6.1.1 节中指出,在大多数编程语言中,大多数二元算术运算符都是左关联的。然而,在6.1.4 节中,我们还指出,大多数编译器可以自由地以任意顺序评估二元运算符的操作数。这些说法是否矛盾?为什么或为什么不矛盾?
6.1 We noted in Section 6.1.1 that most binary arithmetic operators are left-associative in most programming languages. In Section 6.1.4, however, we also noted that most compilers are free to evaluate the operands of a binary operator in either order. Are these statements contradictory? Why or why not?
6.2如图 6.1所示,Fortran 和 Pascal 为一元和二元减法赋予了相同的优先级。这是否会导致某些表达式的求值不直观?为什么会这样?
6.2 As noted in Figure 6.1, Fortran and Pascal give unary and binary minus the same level of precedence. Is this likely to lead to nonintuitive evaluations of certain expressions? Why or why not?
6.3 在示例 6.9中,我们描述了 Pascal 程序中的一个常见错误,该错误是由and和or的优先级与算术运算符的优先级相当而引起的。说明在基于流的 C++ I/O 中如何出现类似的问题(如第 C-8.7.3 节所述)。(提示:考虑 << 和 >> 的优先级,以及图 6.1中 C 列中它们下方出现的运算符。)
6.3 In Example 6.9 we described a common error in Pascal programs caused by the fact that and and or have precedence comparable to that of the arithmetic operators. Show how a similar problem can arise in the stream-based I/O of C++ (described in Section C-8.7.3). (Hint: Consider the precedence of << and >>, and the operators that appear below them in the C column of Figure 6.1.)
6.4 将下列表达式翻译成后缀和前缀表示法:
6.4 Translate the following expression into postfix and prefix notation:
一元否定需要一个特殊符号吗?
Do you need a special symbol for unary negation?
6.5 在 Lisp 中,大多数算术运算符被定义为接受两个或更多个参数,而不是严格地接受两个参数。因此(* 2 3 4 5)的计算结果为 120,而(- 16 9 4)的计算结果为 3。说明括号对于消除 Lisp 中算术表达式的歧义是必需的(换句话说,给出一个表达式的例子,当删除括号时,其含义不明确)。
在第 6.1.1 节中,我们声称前缀或后缀表示法不会出现优先级和结合性问题。重新表述这一说法,以明确隐藏的假设。
6.5 In Lisp, most of the arithmetic operators are defined to take two or more arguments, rather than strictly two. Thus (* 2 3 4 5) evaluates to 120, and (- 16 9 4) evaluates to 3. Show that parentheses are necessary to disambiguate arithmetic expressions in Lisp (in other words, give an example of an expression whose meaning is unclear when parentheses are removed).
In Section 6.1.1 we claimed that issues of precedence and associativity do not arise with prefix or postfix notation. Reword this claim to make explicit the hidden assumption.
6.6 示例 6.33声称“对于某些x 值,(0.1 + x) * 10.0和1.0 + (x * 10.0)可能会相差多达 25%,即使0.1和x 的数量级相同。”验证此说法。(警告:如果您使用的是 x86 处理器,请注意浮点计算(即使是单精度变量)也是在内部以 80 位精度执行的。只有当中间结果存储到内存(精度有限)并再次读回时,才会出现舍入误差。)
6.6 Example 6.33 claims that “For certain values of x, (0.1 + x) * 10.0 and 1.0 + (x * 10.0) can differ by as much as 25%, even when 0.1 and x are of the same magnitude.” Verify this claim. (Warning: If you're using an x86 processor, be aware that floating-point calculations [even on single-precision variables] are performed internally with 80 bits of precision. Roundoff errors will appear only when intermediate results are stored out to memory [with limited precision] and read back in again.)
6.7在 C 语言中 &(&i)是否有效?解释一下。
6.7 Is &(&i) ever valid in C? Explain.
6.8 采用变量引用模型的语言也倾向于采用自动垃圾收集。这不仅仅是巧合吗?解释一下。
6.8 Languages that employ a reference model of variables also tend to employ automatic garbage collection. Is this more than a coincidence? Explain.
6.9 在第 6.1.2 节(“正交性”)中,我们注意到 C 使用 = 进行赋值,使用==进行相等性测试。语言设计者指出:“由于在典型的 C 程序中,赋值的频率大约是相等性测试的两倍,因此运算符的长度应为相等性的一半”[ KR88,第 17 页]。您如何看待这一理由?
6.9 In Section 6.1.2 (“Orthogonality”), we noted that C uses = for assignment and == for equality testing. The language designers state: “Since assignment is about twice as frequent as equality testing in typical C programs, it's appropriate that the operator be half as long” [KR88, p. 17]. What do you think of this rationale?
6.10 考虑一种语言实现,我们希望在其中捕获未初始化变量的每次使用。在6.1.3 节中,我们指出,对于每个可能的位模式都代表有效值的类型,必须使用额外的空间来保存已初始化/未初始化标志。在这样的系统中,动态检查可能很昂贵,主要是因为访问标志需要地址计算。我们可以在常见情况下通过让编译器生成代码来自动用不同的标记值初始化每个变量来降低成本。如果我们在某个时候发现变量的值与标记值不同,那么该变量一定已被初始化。如果它的值是标记值,我们必须仔细检查该标志。描述初始化标志的合理分配策略,并展示动态检查所需的汇编语言序列(使用和不使用标记值)。
6.10 Consider a language implementation in which we wish to catch every use of an uninitialized variable. In Section 6.1.3 we noted that for types in which every possible bit pattern represents a valid value, extra space must be used to hold an initialized/uninitialized flag. Dynamic checks in such a system can be expensive, largely because of the address calculations needed to access the flags. We can reduce the cost in the common case by having the compiler generate code to automatically initialize every variable with a distinguished sentinel value. If at some point we find that a variable's value is different from the sentinel, then that variable must have been initialized. If its value is the sentinel, we must double-check the flag. Describe a plausible allocation strategy for initialization flags, and show the assembly language sequences that would be required for dynamic checks, with and without the use of sentinels.
6.11 根据以下上下文无关文法编写一个属性文法,该文法累积布尔表达式的跳转代码(带短路)转换为条件的合成属性代码,然后使用此属性生成 if 语句的代码。stmt → if条件then stmt else stmt → other_stmt条件→ c_term |条件或c_term c_term → c_factor | c_term和c_factor c_factor → ident关系ident | (条件) | 非 (条件)关系→ < | <= | = | <> | > | >=您可以假设otherstmt和 ident 节点的 code 属性已经初始化。(有关提示,请参阅 Fischer 等人的编译器书籍 [ FCL10,第 14.1.4 节]。)
6.11 Write an attribute grammar, based on the following context-free grammar, that accumulates jump code for Boolean expressions (with short-circuiting) into a synthesized attribute code of condition, and then uses this attribute to generate code for if statements.
stmt → if condition then stmt else stmt
→ other_stmt
condition → c_term | condition or c_term
c_term → c_factor | c_term and c_factor
c_factor → ident relation ident | ( condition ) | not ( condition )
relation → < | <= | = | <> | > | >=
You may assume that the code attribute has already been initialized for otherstmt and ident nodes. (For hints, see Fischer et al.'s compiler book [FCL10, Sec. 14.1.4].)
6.12 描述一个程序员可能希望避免布尔表达式的短路求值的情况。
6.12 Describe a plausible scenario in which a programmer might wish to avoid short-circuit evaluation of a Boolean expression.
6.13 Algol 60 和 Algol 68 均未对布尔表达式使用短路求值。然而,在这两种语言中,if…then…else结构都可以用作表达式。说明如何使用if…then…else来实现短路求值的效果。
6.13 Neither Algol 60 nor Algol 68 employs short-circuit evaluation for Boolean expressions. In both languages, however, an if… then … else construct can be used as an expression. Show how to use if…then …else to achieve the effect ofshort-circuit evaluation.
6.14 考虑以下 C 语言表达式:a/b > 0 && b/a > 0。当a为零时,计算该表达式的结果是什么?当b为零时,结果又是什么?尝试设计一种语言,保证当a或b (但不是两者)为零时,该表达式的计算结果为假,这是否有意义?解释你的答案。
6.14 Consider the following expression in C: a/b > 0 && b/a > 0. What will be the result of evaluating this expression when a is zero? What will be the result when b is zero? Would it make sense to try to design a language in which this expression is guaranteed to evaluate to false when either a or b (but not both) is zero? Explain your answer.
6.15如 第 6.4.2 节所述,当 case 语句中的控制表达式未出现在分支标签中时,不同语言的处理方式有所不同。C 和 Fortran 90 表示该语句无效。Pascal 和 Modula 表示这会导致动态语义错误。Ada 表示标签必须覆盖表达式类型的所有可能值,因此运行时永远不会出现缺失值的问题。这些替代方案之间的权衡是什么?您更喜欢哪一个?为什么?
6.15 As noted in Section 6.4.2, languages vary in how they handle the situation in which the controlling expression in a case statement does not appear among the labels on the arms. C and Fortran 90 say the statement has no effect. Pascal and Modula say it results in a dynamic semantic error. Ada says that the labels must cover all possible values for the type of the expression, so the question of a missing value can never arise at run time. What are the tradeoffs among these alternatives? Which do you prefer? Why?
6.16 示例 6.64中提到的 for 和 while 循环的等价性并不准确。请给出一个它不成立的例子。提示:思考 continue 语句。
6.16 The equivalence of for and while loops, mentioned in Example 6.64, is not precise. Give an example in which it breaks down. Hint: think about the continue statement.
6.17用 C# 或 Ruby 编写与 图 6.5等效的程序。编写第二个版本,执行按序枚举,而不是按预序枚举。
6.17 Write the equivalent of Figure 6.5 in C# or Ruby. Write a second version that performs an in-order enumeration, rather than preorder.
6.18修改 图 6.6的算法,使其执行按序枚举,而不是按预序。
6.18 Revise the algorithm of Figure 6.6 so that it performs an in-order enumeration, rather than preorder.
6.19编写一个 C++ 前序迭代器,为 示例 6.69中的循环提供树节点。您需要知道(或学习)如何在 C++ 中使用指针、引用、内部类和运算符重载。为了(相对)简单起见,您可以假设树节点中的数据始终是 int;这样就无需使用泛型。您可能希望使用 C++ 标准库中的堆栈抽象。
6.19 Write a C++ preorder iterator to supply tree nodes to the loop in Example 6.69. You will need to know (or learn) how to use pointers, references, inner classes, and operator overloading in C++. For the sake of (relative) simplicity, you may assume that the data in a tree node is always an int; this will save you the need to use generics. You may want to use the stack abstraction from the C++ standard library.
6.20 为示例 6.73中使用的tree_iter类型(struct)和ti_create、ti_done、ti_next、ti_val和ti_delete函数编写代码。
6.20 Write code for the tree_iter type (struct) and the ti_create, ti_done, ti_next, ti_val, and ti_delete functions employed in Example 6.73.
6.21 用 C#、Python 或 Ruby 编写一个迭代器,产生
6.21 Write, in C#, Python, or Ruby, an iterator that yields
(a) all permutations of the integers 1 ..n
(b)从 1 .. n(0 ≤ k ≤ n)范围中k 个整数的所有组合。
(b) all combinations of k integers from the range 1 ..n (0 ≤ k ≤ n).
您可以使用列表或数组来表示排列和组合。
You may represent your permutations and combinations using either a list or an array.
6.22 使用迭代器构建一个程序,以某种顺序输出所有结构不同的n 个节点的二叉树。如果两棵树的节点数不同,或者它们的左子树或右子树的结构不同,则认为这两棵树的结构不同。例如,有五棵结构不同的三节点树:
6.22 Use iterators to construct a program that outputs (in some order) all structurally distinct binary trees of n nodes. Two trees are considered structurally distinct if they have different numbers of nodes or if their left or right subtrees are structurally distinct. There are, for example, five structurally distinct trees of three nodes:
这些最容易以“点括号形式”输出:(((.).).) ((.(.)).) ((.).(.)) (.((.).)) (.(.(.)))(提示:递归思考!如果需要帮助,请参阅Finkel [ Fin96 ]文本第 2.2 节。)
These are most easily output in “dotted parenthesized form”:
(((.).).)
((.(.)).)
((.).(.))
(.((.).))
(.(.(.)))
(Hint: Think recursively! If you need help, see Section 2.2 of the text by Finkel [Fin96].)
6.23 使用线程在 Java 中构建真正的迭代器。(这需要了解第 13 章中的材料。)使您的解决方案尽可能简洁和通用。特别是,您应该提供标准的Iterator或IEnumerable接口,以便与扩展的 for 循环一起使用,但程序员不必编写这些接口。相反,他或她应该编写一个带有Iterate方法的类,该方法又应该能够调用您也应该提供的Yield方法。评估您的解决方案的成本。它比标准 Java 迭代器对象贵多少?
6.23 Build true iterators in Java using threads. (This requires knowledge of material in Chapter 13.) Make your solution as clean and as general as possible. In particular, you should provide the standard Iterator or IEnumerable interface, for use with extended for loops, but the programmer should not have to write these. Instead, he or she should write a class with an Iterate method, which should in turn be able to call a Yield method, which you should also provide. Evaluate the cost of your solution. How much more expensive is it than standard Java iterator objects?
6.24 在面向表达式的语言(例如 Algol 68 或 Lisp)中,while循环( Lisp 中的do循环)具有表达式值。您认为应该如何确定这个值?(在 Algol 68 和 Lisp 中如何确定?)这个值是面向表达式的无用产物吗?还是有合理的程序可以实际使用它?如果循环中的条件是循环主体永远不会执行,您认为应该发生什么?
6.24 In an expression-oriented language such as Algol 68 or Lisp, a while loop (a do loop in Lisp) has a value as an expression. How do you think this value should be determined? (How is it determined in Algol 68 and Lisp?) Is the value a useless artifact of expression orientation, or are there reasonable programs in which it might actually be used? What do you think should happen if the condition on the loop is such that the body is never executed?
6.25 考虑一个中间测试循环,这里用 C 语言编写,它在输入中查找空行:for (;;) { line = read_line(); if (all_blanks(line)) break; consumer_line(line); }
说明如果没有中期测试循环,如何使用 while 或 do( repeat )循环完成相同的任务。(提示:一种替代方案重复部分代码;另一种引入布尔标志变量。)这些替代方案与中期测试版本相比如何?
6.25 Consider a mid-test loop, here written in C, that looks for blank lines in its input:
for (;;) {
line = read_line();
if (all_blanks(line)) break;
consume_line(line);
}
Show how you might accomplish the same task using a while or do (repeat) loop, if mid-test loops were not available. (Hint: One alternative duplicates part of the code; another introduces a Boolean flag variable.) How do these alternatives compare to the mid-test version?
6.26 Rubin [ Rub87 ] 使用了下面的例子(这里用 C 语言重写)来支持goto语句:int first_zero_row = -1; /* none */ int i, j; for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { if (A[i][j]) goto next; } first_zero_row = i; break; next: ; }该代码的目的是找出一个n × n矩阵的第一个全零行(如果有)。你觉得这个例子有说服力吗?C 语言中有没有好的结构化替代方案?有什么语言吗?
6.26 Rubin [Rub87] used the following example (rewritten here in C) to argue in favor of a goto statement:
int first_zero_row = -1; /* none */
int i, j;
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
if (A[i][j]) goto next;
}
first_zero_row = i;
break;
next: ;
}
The intent of the code is to find the first all-zero row, if any, of an n × n matrix. Do you find the example convincing? Is there a good structured alternative in C? In any language?
6.27 Bentley[ Ben00,第 4 章]对二分查找给出了如下非正式描述:
6.27 Bentley [Ben00, Chap. 4] provides the following informal description of binary search:
我们要确定已排序的数组X[1..N]是否包含元素T ...。二分搜索通过跟踪数组中的某个范围来解决这个问题,如果T位于数组中的任何位置,则它必须位于该范围中。最初,范围是整个数组。通过将其中间元素与T进行比较并丢弃一半范围,可以缩小范围。该过程持续进行,直到在数组中发现T或知道它必须位于的范围为空。
We are to determine whether the sorted array X[1..N] contains the element T…. Binary search solves the problem by keeping track of a range within the array in which T must be if it is anywhere in the array. Initially, the range is the entire array. The range is shrunk by comparing its middle element to T and discarding half the range. The process continues until T is discovered in the array or until the range in which it must lie is known to be empty.
用您最喜欢的命令式编程语言编写二分查找代码。您认为哪种循环结构最有用?注意:当他要求一百多名专业程序员解决这个问题时,本特利发现只有大约 10% 的人第一次就答对了,而且没有进行测试。
Write code for binary search in your favorite imperative programming language. What loop construct(s) did you find to be most useful? NB: when he asked more than a hundred professional programmers to solve this problem, Bentley found that only about 10% got it right the first time, without testing.
6.28 循环不变量是每次迭代时在循环体内的给定点保证为真的条件。循环不变量在公理语义中起着重要作用,公理语义是一种用于证明程序属性的形式化推理系统。以一种不太正式的方式,识别(并写下!)循环不变量的程序员更有可能编写正确的代码。显示上一个练习的解决方案的循环不变量。
(提示:您会发现 < 和 ≤ [或 > 和 ≥] 之间的区别至关重要。)
6.28 A loop invariant is a condition that is guaranteed to be true at a given point within the body of a loop on every iteration. Loop invariants play a major role in axiomatic semantics, a formal reasoning system used to prove properties of programs. In a less formal way, programmers who identify (and write down!) the invariants for their loops are more likely to write correct code. Show the loop invariant(s) for your solution to the preceding exercise.
(Hint: You will find the distinction between < and ≤ [or between > and ≥] to be crucial.)
6.29 如果你已经学习过自动机理论或递归函数理论课程,请解释为什么while循环比for循环更强大。(如果你没有学习过这样的课程,请跳过这个问题!)请注意,我们这里指的是 Ada 风格的for循环,而不是 C 风格的 for 循环。
6.29 If you have taken a course in automata theory or recursive function theory, explain why while loops are strictly more powerful than for loops. (If you haven't had such a course, skip this question!) Note that we're referring here to Ada-style for loops, not C-style.
6.30说明如何计算一般 Fortran 90 风格 do循环的迭代次数。您的代码应以类似汇编的符号编写,并应保证适用于所有有效的界限和步长。小心溢出!(提示:虽然循环的界限和步长可以是正数或负数,但您可以安全地使用无符号整数作为迭代计数。)
6.30 Show how to calculate the number of iterations of a general Fortran 90-style do loop. Your code should be written in an assembler-like notation, and should be guaranteed to work for all valid bounds and step sizes. Be careful of overflow! (Hint: While the bounds and step size of the loop can be either positive or negative, you can safely use an unsigned integer for the iteration count.)
6.31 用 Scheme 或 ML 编写一个尾递归函数来计算n 的阶乘。(提示:你可能需要定义一个“辅助”函数,如第 6.6.1 节所述。)
6.31 Write a tail-recursive function in Scheme or ML to compute n factorial
6.32 是否可以编写经典快速排序算法的尾递归版本?为什么或为什么不可以?
6.32 Is it possible to write a tail-recursive version of the classic quicksort algorithm? Why or why not?
6.33 给出一个 C 语言中的例子,其中内联子程序可能比功能等效的宏快得多。给出另一个宏可能更快的例子。(提示:考虑参数的应用顺序与正常顺序的求值。)
6.33 Give an example in C in which an in-line subroutine may be significantly faster than a functionally equivalent macro. Give another example in which the macro is likely to be faster. (Hint: Think about applicative vs normal-order evaluation of arguments.)
6.34 使用惰性求值(delay和force)在 Scheme 中实现迭代器对象。更具体地说,让迭代器为空列表或由一个元素和一个承诺组成的对,当强制时将返回一个迭代器。给出一个返回迭代器的uptoby函数的代码,以及一个接受一个参数函数和一个迭代器作为参数的for -iter函数的代码。这些应该允许您评估这样的表达式(for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))请注意,与标准 Scheme 的for-each 不同,for-iter不应该要求存在一个包含要迭代的元素的列表; (for-iter f (uptoby 1 n 1))所需的内在空间应该只是O (1),而不是O(n)。
6.34 Use lazy evaluation (delay and force) to implement iterator objects in Scheme. More specifically, let an iterator be either the null list or a pair consisting of an element and a promise which when forced will return an iterator. Give code for an uptoby function that returns an iterator, and a for-iter function that accepts as arguments a one-argument function and an iterator. These should allow you to evaluate such expressions as
(for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))
Note that unlike the standard Scheme for-each, for-iter should not require the existence of a list containing the elements over which to iterate; the intrinsic space required for (for-iter f (uptoby 1 n 1)) should be only O(1), rather than O(n).
6.35 (困难)使用call-with-current-continuation(call/cc)在 Scheme 中实现以下结构化非本地控制传输。(这需要了解第 11 章中的材料。)您可能希望查阅 Scheme 手册,以获取有关call/cc以及define-syntax和dynamic-wind的文档。
6.35 (Difficult) Use call-with-current-continuation (call/cc) to implement the following structured nonlocal control transfers in Scheme. (This requires knowledge of material in Chapter 11.) You will probably want to consult a Scheme manual for documentation not only on call/cc, but on define-syntax and dynamic-wind as well.
(a) 多级返回。以Common Lisp 的catch和throw为范本来设计你的语法。
(a) Multilevel returns. Model your syntax after the catch and throw of Common Lisp.
(b)真正的迭代器。与 练习 6.34类似,迭代器是一个函数,当调用/cc时,它将返回一个空列表或一对由元素和迭代器组成。与上一个练习一样,你的实现应该支持如下表达式
(b) True iterators. In a style reminiscent of Exercise 6.34, let an iterator be a function which when call/cc-ed will return either a null list or a pair consisting of an element and an iterator. As in that previous exercise, your implementation should support expressions like
(for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))但是,练习 6.34
中的 uptoby 实现需要使用delay和force,您应该提供一个迭代器宏(Scheme特殊形式)和一个 Yield 函数,使 uptoby 看起来像一个带有嵌入Yield的普通尾递归函数:(define uptoby (iterator (low high step) (letrec ((helper (lambda (next) (if (> next high) '() (begin ; else clause (yield next) (helper (+ next step))))))) (helper low))))
(for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))
Where the implementation of uptoby in Exercise 6.34 required the use of delay and force, however, you should provide an iterator macro (a Scheme special form) and a yield function that allows uptoby to look like an ordinary tail-recursive function with an embedded yield:
(define uptoby
(iterator (low high step)
(letrec ((helper (lambda (next)
(if (> next high) '()
(begin ; else clause
(yield next)
(helper (+ next step)))))))
(helper low))))
6.36–6.40 更深入。
6.36–6.40 In More Depth.
6.41 循环展开(在练习 C-5.21 和节 C-17.7.1 中描述)是一种代码转换,它复制循环主体并减少迭代次数,从而减少循环开销并通过重新排序指令增加提高处理器流水线性能的机会。展开传统上由编译器的代码改进阶段实现。然而,如果我们需要在编译器无法胜任的系统上“手动优化”时间关键型代码,则可以在源代码级别实现它。不幸的是,如果我们将循环主体复制k次,则必须处理原始循环迭代次数n可能不是 k 的倍数的可能性。用 C 语言编写,设k = 4,我们可以将练习 C-5.21 的主循环从i = 0; do { sum += A[i]; squares += A[i] * A[i]; i++; } while (i < N);转换为i = 0; j = N/4;执行 { sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; } while (--j > 0);执行 { sum += A[i]; squares += A[i] * A[i]; i++; } while (i < N);
1983 年,卢卡斯影业的汤姆·达夫 (Tom Duff) 意识到,通过在 C 语言中交错使用switch语句和循环,可以“简化”此类代码。 这个结果令人吃惊,但却是完全有效的 C 语言。 编程传说中将此称为“达夫装置”:i = 0; j = (N+3)/4; switch (N%4) { case 0: do{ sum += A[i]; squares += A[i] * A[i]; i++; case 3: sum += A[i]; squares += A[i] * A[i]; i++; case 2: sum += A[i]; squares += A[i] * A[i]; i++; case 1: sum += A[i]; squares += A[i] * A[i]; i++; } while (--j > 0); }达夫怀着“骄傲与厌恶”的心情宣布了他的发现。他指出,“很多人……都说 C 语言最糟糕的特性是 switch 不会在每个 case 标签前自动中断。这段代码在这场争论中形成了某种论据,但我不确定它是赞成还是反对。”你怎么看?以这种方式交错循环和 switch 是否合理?编程语言应该允许这样做吗?自动 fall-through 是否是个好主意?
6.41 Loop unrolling (described in Exercise C-5.21 and Section C-17.7.1) is a code transformation that replicates the body of a loop and reduces the number of iterations, thereby decreasing loop overhead and increasing opportunities to improve the performance of the processor pipeline by reordering instructions. Unrolling is traditionally implemented by the code improvement phase of a compiler. It can be implemented at source level, however, if we are faced with the prospect of “hand optimizing” time-critical code on a system whose compiler is not up to the task. Unfortunately, if we replicate the body of a loop k times, we must deal with the possibility that the original number of loop iterations, n, may not be a multiple of k. Writing in C, and letting k = 4, we might transform the main loop of Exercise C-5.21 from
i = 0;
do {
sum += A[i]; squares += A[i] * A[i]; i++;
} while (i < N);
to
i = 0; j = N/4;
do {
sum += A[i]; squares += A[i] * A[i]; i++;
sum += A[i]; squares += A[i] * A[i]; i++;
sum += A[i]; squares += A[i] * A[i]; i++;
sum += A[i]; squares += A[i] * A[i]; i++;
} while (--j > 0);
do {
sum += A[i]; squares += A[i] * A[i]; i++;
} while (i < N);
In 1983, Tom Duff of Lucasfilm realized that code of this sort can be “simplified” in C by interleaving a switch statement and a loop. The result is rather startling, but perfectly valid C. It's known in programming folklore as “Duff's device”:
i = 0; j = (N+3)/4;
switch (N%4) {
case 0: do{ sum += A[i]; squares += A[i] * A[i]; i++;
case 3: sum += A[i]; squares += A[i] * A[i]; i++;
case 2: sum += A[i]; squares += A[i] * A[i]; i++;
case 1: sum += A[i]; squares += A[i] * A[i]; i++;
} while (--j > 0);
}
Duff announced his discovery with “a combination of pride and revulsion.” He noted that “Many people… have said that the worst feature of C is that switches don't break automatically before each case label. This code forms some sort of argument in that debate, but I'm not sure whether it's for or against.” What do you think? Is it reasonable to interleave a loop and a switch in this way? Should a programming language permit it? Is automatic fall-through ever a good idea?
6.42 使用你最喜欢的语言和编译器,研究子程序参数的求值顺序。它们通常是从左到右还是从右到左求值?它们是否曾经以其他顺序求值?(你能确定吗?)编写一个程序,其中顺序会对计算结果产生影响。
6.42 Using your favorite language and compiler, investigate the order of evaluation of subroutine parameters. Are they usually evaluated left-to-right or right-to-left? Are they ever evaluated in the other order? (Can you be sure?) Write a program in which the order makes a difference in the results of the computation.
6.43考虑 Pascal、C、Java、C# 和 Common Lisp 所采用的算术溢出的不同方法,如 第 6.1.4 节所述。推测语言设计目标的差异可能导致设计者采用他们所采用的方法。
6.43 Consider the different approaches to arithmetic overflow adopted by Pascal, C, Java, C#, and Common Lisp, as described in Section 6.1.4. Speculate as to the differences in language design goals that might have caused the designers to adopt the approaches they did.
6.44 进一步了解容器类及其支持的设计模式(结构化编程习惯)。探索 C++、Java 和 C# 标准容器库之间的相似之处和不同之处。您觉得这些库中哪一个最有吸引力?为什么?
6.44 Learn more about container classes and the design patterns (structured programming idioms) they support. Explore the similarities and differences among the standard container libraries of C++, Java, and C#. Which of these libraries do you find the most appealing? Why?
6.45 在示例中 6.43 和6.72 中我们提出 Ruby proc(传递给函数的块,作为隐式额外参数)“大致”等同于 lambda 表达式。事实证明,Ruby 既有 procs也有lambda 表达式,它们几乎相同,但又不完全相同。了解详细信息和它们的开发历史。在什么情况下 proc 和 lambda 的行为会有所不同,为什么?
6.45 In Examples 6.43 and 6.72 we suggested that a Ruby proc (a block, passed to a function as an implicit extra argument) was “roughly” equivalent to a lambda expression. As it turns out, Ruby has both procs and lambda expressions, and they're almost—but not quite—the same. Learn about the details, and the history of their development. In what situations will a proc and a lambda behave differently, and why?
6.46 大型系统最流行的习语之一是所谓的访问者模式。它有几种用途,其中一种类似于示例 6.70和6.71中的“使用一流函数进行迭代”习语。简而言之,容器类的元素提供一个 accept 方法,该方法期望一个实现访问者接口的对象作为参数。这个接口反过来有一个名为visit 的方法,它期望一个元素类型的参数。要遍历集合,我们在访问者对象的visit方法中实现“循环体”。此对象构成第 3.6.3 节中描述的闭包。visit 所需的任何信息(除了“循环索引”元素的标识之外)都可以封装在对象的字段中。集合的迭代器方法将访问者对象传递给每个元素的accept方法。每个元素反过来调用访问者对象的 visit 方法,并将自身作为参数传递。
了解有关访问者模式的更多信息。使用它来实现集合的迭代器 — 例如二叉树的前序、中序和后序遍历。访问者与基于迭代器的等效代码相比如何?它们是否添加了新功能?除了迭代之外,访问者还有哪些用处?
6.46 One of the most popular idioms for large-scale systems is the so-called visitor pattern. It has several uses, one of which resembles the “iterating with first-class functions” idiom of Examples 6.70 and 6.71. Briefly, elements of a container class provide an accept method that expects as argument an object that implements the visitor interface. This interface in turn has a method named visit that expects an argument of element type. To iterate over a collection, we implement the “loop body” in the visit method of a visitor object. This object constitutes a closure of the sort described in Section 3.6.3. Any information that visit needs (beyond the identify of the “loop index” element) can be encapsulated in the object's fields. An iterator method for the collection passes the visitor object to the accept method of each element. Each element in turn calls the visit method of the visitor object, passing itself as argument.
Learn more about the visitor pattern. Use it to implement iterators for a collection—preorder, inorder, and postorder traversals of a binary tree, for example. How do visitors compare with equivalent iterator-based code? Do they add new functionality? What else are visitors good for, in addition to iteration?
6.47–6.50 更深入。
6.47–6.50 In More Depth.
本章讨论的许多问题在有关编程语言历史的论文中都有突出的体现。第1 章的参考书目注释中可以找到几篇这样的论文。在 Feuer 和 Gehani 编辑的论文集 [ FG84 ]中可以找到 15 篇比较 Ada、C 和 Pascal 的论文。各个语言的参考资料可以在附录 A中找到。
Many of the issues discussed in this chapter feature prominently in papers on the history of programming languages. Pointers to several such papers can be found in the Bibliographic Notes for Chapter 1. Fifteen papers comparing Ada, C, and Pascal can be found in the collection edited by Feuer and Gehani [FG84]. References for individual languages can be found in Appendix A.
Niklaus Wirth 在过去 30 年中负责了一系列有影响力的语言,包括 Pascal [ Wir71 ]、其前身 Algol W [ WH66 ] 以及后继者 Modula [ Wir77b ]、Modula-2 [ Wir85b ] 和 Oberon [ Wir88b ]。Algol W 的 case 语句由 Hoare [ Hoa81 ]提出。Bernstein [ Ber85 ] 考虑了 case 的各种替代实现,包括适用于由多个密集的值“簇”组成的标签集的多级版本。Guarded 命令(第 C-6.7 节)由 Dijkstra [ Dij75 ] 提出。Duff 的设备(Exploration 6.41)最初于 1984 年 5 月发布到早期的在线讨论组系统 netnews。原始帖子似乎是丢失了,但达夫对此的评论可以在许多互联网网站上找到,包括www.lysator.liu.se/c/duffs-device.html。
Niklaus Wirth has been responsible for a series of influential languages over a 30-year period, including Pascal [Wir71], its predecessor Algol W [WH66], and the successors Modula [Wir77b], Modula-2 [Wir85b], and Oberon [Wir88b]. The case statement of Algol W is due to Hoare [Hoa81]. Bernstein [Ber85] considers a variety of alternative implementations for case, including multilevel versions appropriate for label sets consisting of several dense “clusters” of values. Guarded commands (Section C-6.7) are due to Dijkstra [Dij75]. Duff's device (Exploration 6.41) was originally posted to netnews, an early on-line discussion group system, in May of 1984. The original posting appears to have been lost, but Duff's commentary on it can be found at many Internet sites, including www.lysator.liu.se/c/duffs-device.html.
关于 goto 语句的所谓优点或缺点的争论至少可以追溯到 20 世纪 60 年代初期,但在 Dijkstra 于 1968 年发表的一篇文章(“Go To 语句被认为有害” [ Dij68b ])之后,争论变得更加激烈。20 世纪 70 年代的结构化编程运动以 Dahl、Dijkstra 和 Hoare 的文章命名 [ DDH72 ]。Rubin 在 1987 年发表了一封反对信(“'GOTO 被认为有害' 被认为有害” [ Rub87 ];练习 6.26)引起了一连串的回应。
Debate over the supposed merits or evils of the goto statement dates from at least the early 1960s, but became a good bit more heated in the wake of a 1968 article by Dijkstra (“Go To Statement Considered Harmful” [Dij68b]). The structured programming movement of the 1970s took its name from the text of Dahl, Dijkstra, and Hoare [DDH72]. A dissenting letter by Rubin in 1987 (“'GOTO Considered Harmful' Considered Harmful” [Rub87]; Exercise 6.26) elicited a flurry of responses.
本章中所说的“变量的引用模型”在 Clu 中被称为“对象模型”;Liskov 和 Guttag 在他们关于抽象和规范的文本 [ LG86 ] 的第 2.3和 2.4.2节中对其进行了描述。Liskov 等人的一篇文章 [ LSAS77 ] 以及 Liskov 和 Guttag 文本的第 6 章描述了 Clu 迭代器。Griswold和 Griswold [ GG96 ] 在文本的第 11 章和第14 章中讨论了图标生成器。Thomas等人 [ TFH13 ] 在文本的第 4 章中讨论了 Ruby 块、过程和迭代器。练习 6.22中的树枚举算法最初由 Solomon 和 Finkel [ SF80 ]提出(没有迭代器)。
What has been called the “reference model of variables” in this chapter is called the “object model” in Clu; Liskov and Guttag describe it in Sections 2.3 and 2.4.2 of their text on abstraction and specification [LG86]. Clu iterators are described in an article by Liskov et al. [LSAS77], and in Chapter 6 of the Liskov and Guttag text. Icon generators are discussed in Chapters 11 and 14 of the text by Griswold and Griswold [GG96]. Ruby blocks, procs, and iterators are discussed in Chapter 4 of the text by Thomas et al. [TFH13]. The tree-enumeration algorithm of Exercise 6.22 was originally presented (without iterators) by Solomon and Finkel [SF80].
有几篇文章讨论了使用不变量(练习 6.28 )作为编写正确程序的工具。特别值得注意的是 Dijkstra [ Dij76 ] 和 Gries [ Gri81 ]的作品。Kernighan 和 Plauger 对编写优秀程序的艺术进行了更为非正式的讨论 [ KP78 ]。
Several texts discuss the use of invariants (Exercise 6.28) as a tool for writing correct programs. Particularly noteworthy are the works of Dijkstra [Dij76] and Gries [Gri81]. Kernighan and Plauger provide a more informal discussion of the art of writing good programs [KP78].
Blizzard [ SFL + 94 ] 和 Shasta [ SG96 ] 软件分布式共享内存 (S-DSM) 系统利用了哨兵 (练习 6.10 )。我们将在13.2.1 节讨论 S-DSM 。
The Blizzard [SFL+94] and Shasta [SG96] systems for software distributed shared memory (S-DSM) make use of sentinels (Exercise 6.10). We will discuss S-DSM in Section 13.2.1.
Michaelson [ Mic89,第 8 章] 提供了应用顺序、正常顺序和惰性求值的通俗易懂的形式化处理。记忆化的概念最初由 Michie [ Mic68 ] 提出。Friedman、Wand 和 Haynes 对延续传递风格进行了精彩的讨论 [ FWH01,第7-8章]。
Michaelson [Mic89, Chap. 8] provides an accessible formal treatment of applicative-order, normal-order, and lazy evaluation. The concept of memoization is originally due to Michie [Mic68]. Friedman, Wand, and Haynes provide an excellent discussion of continuation-passing style [FWH01, Chaps. 7–8].
大多数编程语言都包含表达式和/或对象的类型概念。1类型有几个重要用途:
Most programming languages include a notion of type for expressions and/or objects.1 Types serve several important purposes:
3. 如果在源程序中明确指定类型(许多语言都是如此,但并非所有语言都是如此),它们通常可以使程序更易于阅读和理解。实际上,它们充当了程式化的文档,其正确性由编译器检查。(另一方面,对这种文档的需求有时会使程序更难编写。)
3. If types are specified explicitly in the source program (as they are in many but not all languages), they can often make the program easier to read and understand. In effect, they serve as stylized documentation, whose correctness is checked by the compiler. (On the flip side, the need for this documentation can sometimes make the program harder to write.)
第 7.1 节 更仔细地研究类型的含义和目的。它给出了一些基本定义,并介绍了多态性和正交性的概念。第 7.2 节更仔细地研究了类型检查;特别是,它考虑了类型等价性(什么时候我们可以说两种类型是相同的?)、类型兼容性(什么时候我们可以在给定的上下文中使用给定类型的值?)和类型推断(我们如何从表达式的组件类型和周围上下文的类型推断出表达式的类型?)。
Section 7.1 looks more closely at the meaning and purpose of types. It presents some basic definitions, and introduces the notions of polymorphism and orthogonality. Section 7.2 takes a closer look at type checking; in particular, it considers type equivalence (when can we say that two types are the same?), type compatibility (when can we use a value of a given type in a given context?), and type inference (how do we deduce the type of an expression from the types of its components and that of the surrounding context?).
作为多态性和复杂推理的一个例子,第 7.2.4 节概述了 ML 的类型系统,它在很大程度上将编译的效率和早期错误报告与解释的便利性和灵活性结合在一起。我们在第 7.3 节继续研究多态性,特别强调泛型,它允许将代码主体显式地参数化为多种类型。最后,在第 7.4 节中,我们考虑比较两个复杂对象是否相等或将一个对象赋值给另一个对象的含义。在第 8 章中,我们将考虑一些最重要的复合类型的句法、语义和语用问题:记录、数组、字符串、集合、指针、列表和文件。
As an example of both polymorphism and sophisticated inference, Section 7.2.4 surveys the type system of ML, which combines, to a large extent, the efficiency and early error reporting of compilation with the convenience and flexibility of interpretation. We continue the study of polymorphism in Section 7.3, with a particular emphasis on generics, which allow a body of code to be parameterized explicitly for multiple types. Finally, in Section 7.4, we consider what it means to compare two complex objects for equality, or to assign one into the other. In Chapter 8 we will consider syntactic, semantic, and pragmatic issues for some of the most important composite types: records, arrays, strings, sets, pointers, lists, and files.
计算机硬件可以以多种不同方式解释内存中的位:指令、地址、字符以及不同长度的整数和浮点数。但是,位本身是无类型的:大多数机器上的硬件不会尝试跟踪哪些解释对应于内存中的哪些位置。汇编语言反映了这种无类型的现象:任何类型的操作都可以应用于任意位置的值。相比之下,高级语言几乎总是将类型与值相关联,以提供上面提到的上下文信息和错误检查。
Computer hardware can interpret bits in memory in several different ways: as instructions, addresses, characters, and integer and floating-point numbers of various lengths. The bits themselves, however, are untyped: the hardware on most machines makes no attempt to keep track of which interpretations correspond to which locations in memory. Assembly languages reflect this lack of typing: operations of any kind can be applied to values in arbitrary locations. High-level languages, by contrast, almost always associate types with values, to provide the contextual information and error checking alluded to above.
通俗地说,类型系统包括 (1) 定义类型并将其与某些语言结构关联的机制,以及 (2) 一组类型等价、类型兼容性和类型推断的规则。必须具有类型的结构恰恰是具有值或可以引用具有值的对象的结构。这些结构包括命名常量、变量、记录字段、参数,有时还有子程序;文字常量(例如17、3.14、“foo”);以及包含这些内容的更复杂的表达式。类型等价规则确定两个值的类型何时相同。类型兼容性规则确定何时可以在给定上下文中使用给定类型的值。类型推断规则根据表达式组成部分的类型或(有时)表达式的周围环境。在具有多态变量或参数的语言中,区分引用或指针的类型和它所引用的对象的类型可能很重要:给定的名称可能在不同时间引用不同类型的对象。
Informally, a type system consists of (1) a mechanism to define types and associate them with certain language constructs, and (2) a set of rules for type equivalence, type compatibility, and type inference. The constructs that must have types are precisely those that have values, or that can refer to objects that have values. These constructs include named constants, variables, record fields, parameters, and sometimes subroutines; literal constants (e.g., 17, 3.14, “foo”); and more complicated expressions containing these. Type equivalence rules determine when the types of two values are the same. Type compatibility rules determine when a value of a given type can be used in a given context. Type inference rules define the type of an expression based on the types of its constituent parts or (sometimes) the surrounding context. In a language with polymorphic variables or parameters, it may be important to distinguish between the type of a reference or pointer and the type of the object to which it refers: a given name may refer to objects of different types at different times.
在某些语言中,子程序被认为具有类型,但在其他语言中则不具有类型。如果子程序是第一类或第二类值(即,如果它们可以作为参数传递、由函数返回或存储在变量中),则它们需要具有类型。在每种情况下,语言中都有一个构造,其值是动态确定的子程序;类型信息允许语言将可接受的值集限制为提供特定子程序接口的值(即特定数量和类型的参数)。在从不动态创建对子程序的引用的静态作用域语言中(子程序始终是第三类值),编译器始终可以识别名称所引用的子程序,并且可以确保正确调用该例程,而不必采用子程序类型的正式概念。
Subroutines are considered to have types in some languages, but not in others. Subroutines need to have types if they are first- or second-class values (i.e., if they can be passed as parameters, returned by functions, or stored in variables). In each of these cases there is a construct in the language whose value is a dynamically determined subroutine; type information allows the language to limit the set of acceptable values to those that provide a particular subroutine interface (i.e., particular numbers and types of parameters). In a statically scoped language that never creates references to subroutines dynamically (one in which subroutines are always third-class values), the compiler can always identify the subroutine to which a name refers, and can ensure that the routine is called correctly without necessarily employing a formal notion of subroutine types.
类型检查是确保程序遵守语言的类型兼容性规则的过程。违反规则的行为称为类型冲突。如果一种语言以语言实现可以强制执行的方式禁止对任何不支持该操作的对象应用任何操作,则该语言被称为强类型语言。如果一种语言是强类型的并且可以在编译时执行类型检查,则该语言被称为静态类型语言。从最严格的意义上讲,很少有语言是静态类型的。在实践中,这个术语通常适用于大多数类型检查可以在编译时执行,其余类型检查可以在运行时执行的语言。
Type checking is the process of ensuring that a program obeys the language's type compatibility rules. A violation of the rules is known as a type clash. A language is said to be strongly typed if it prohibits, in a way that the language implementation can enforce, the application of any operation to any object that is not intended to support that operation. A language is said to be statically typed if it is strongly typed and type checking can be performed at compile time. In the strictest sense of the term, few languages are statically typed. In practice, the term is often applied to languages in which most type checking can be performed at compile time, and the rest can be performed at run time.
自 20 世纪 70 年代中期以来,大多数新开发的语言都趋向于强类型(尽管不一定是静态类型)。有趣的是,C 语言的每个后续版本都变得更加强类型,尽管仍然存在各种漏洞;这些漏洞包括联合、非转换类型转换、具有可变数量参数的子例程以及指针和数组的互操作性(将在8.5.1 节中讨论)。C 的实现很少在运行时检查任何东西。
Since the mid 1970s, most newly developed languages have tended to be strongly (though not necessarily statically) typed. Interestingly, C has become more strongly typed with each successive version of the language, though various loopholes remain; these include unions, nonconverting type casts, subroutines with variable numbers of parameters, and the interoperability of pointers and arrays (to be discussed in Section 8.5.1). Implementations of C rarely check anything at run time.
动态(运行时)类型检查可以看作是一种后期绑定,并且往往出现在将其他问题延迟到运行时的语言中。因此,静态类型是性能导向型语言的常态;动态类型在旨在简化编程的语言中更为常见。Lisp 和 Smalltalk 是动态(尽管是强)类型的。大多数脚本语言也是动态类型的;一些(例如 Python 和 Ruby)是强类型的。具有动态作用域的语言通常是动态类型的(或根本没有类型):如果编译器无法识别名称所指的对象,它通常也无法确定对象的类型。
Dynamic (run-time) type checking can be seen as a form of late binding, and tends to be found in languages that delay other issues until run time as well. Static typing is thus the norm in languages intended for performance; dynamic typing is more common in languages intended for ease of programming. Lisp and Smalltalk are dynamically (though strongly) typed. Most scripting languages are also dynamically typed; some (e.g., Python and Ruby) are strongly typed. Languages with dynamic scoping are generally dynamically typed (or not typed at all): if the compiler can't identify the object to which a name refers, it usually can't determine the type of the object either.
虽然每个程序员至少对“类型”的含义有一个非正式的概念,但这个概念可以用几种不同的方式形式化。其中最流行的三种观点是,我们可以称之为外延观点、结构观点和基于抽象的观点。从外延的角度来看,类型只是一组值。如果一个值属于该集合,它就具有给定类型;如果一个对象的值保证在该集合中,它就具有给定类型。从结构的角度来看,类型要么是一小组内置类型(整数、字符、布尔、实数等;也称为原始类型或预定义类型)之一,要么是由将类型构造函数(记录、数组、集合等)应用于一个或多个更简单的类型。(“构造函数”一词的这种用法与面向对象语言的初始化函数无关。它也与 ML 中的术语用法有更微妙的不同。)从基于抽象的角度来看,类型是由一组具有明确定义且相互一致的语义的操作组成的接口。对于程序员和语言设计者来说,类型也可能反映这些观点的混合。
While every programmer has at least an informal notion of what is meant by “type,” that notion can be formalized in several different ways. Three of the most popular are what we might call the denotational, structural, and abstraction-based points of view. From the denotational point of view, a type is simply a set of values. A value has a given type if it belongs to the set; an object has a given type if its value is guaranteed to be in the set. From the structural point of view, a type is either one of a small collection of built-in types (integer, character, Boolean, real, etc.; also called primitive or predefined types), or a composite type created by applying a type constructor (record, array, set, etc.) to one or more simpler types. (This use of the term “constructor” is unrelated to the initialization functions of object-oriented languages. It also differs in a more subtle way from the use of the term in ML.) From the abstraction-based point of view, a type is an interface consisting of a set of operations with well-defined and mutually consistent semantics. For both programmers and language designers, types may also reflect a mixture of these viewpoints.
在指称语义(形式化程序含义的几种方法之一)中,一组值称为域。类型是域,表达式的含义是来自表示表达式类型的域的值。有些域(例如整数)简单且熟悉,其他则更复杂。数组可以看作是来自其元素为函数的域的值;这些函数中的每一个都将某些有限索引类型(通常是整数的子集)的值映射到其他元素类型的值。事实证明,指称语义可以将类型与程序中的所有内容相关联,甚至是具有副作用的语句。赋值语句的含义是来自更高级函数域的值,其每个元素将一个存储(从名称到表示内存当前内容的值的映射)映射到另一个存储,该存储表示赋值后的内存内容。
In denotational semantics (one of several ways to formalize the meaning of programs), a set of values is known as a domain. Types are domains, and the meaning of an expression is a value from the domain that represents the expression's type. Some domains—the integers, for example—are simple and familiar. Others are more complex. An array can be thought of as a value from a domain whose elements are functions; each of these functions maps values from some finite index type (typically a subset of the integers) to values of some other element type. As it turns out, denotational semantics can associate a type with everything in a program—even statements with side effects. The meaning of an assignment statement is a value from a domain of higher-level functions, each of whose elements maps a store—a mapping from names to values that represents the current contents of memory—to another store, which represents the contents of memory after the assignment.
类型的表示视图的优点之一是,它允许我们在许多情况下用集合上的数学运算来描述用户定义的复合类型(记录、数组等)。我们将在第7.1.4 节“复合类型”下再次提到这些运算。由于类型的表示视图基于数学对象,因此它通常会忽略精度和字长有限等实现问题。这种限制并没有乍一看那么严重:对算术溢出等错误的检查通常是在语言的类型系统之外实现的。它们会导致运行时错误,但这种错误不称为类型冲突。
One of the nice things about the denotational view of types is that it allows us in many cases to describe user-defined composite types (records, arrays, etc.) in terms of mathematical operations on sets. We will allude to these operations again under “Composite Types” in Section 7.1.4. Because it is based on mathematical objects, the denotational view of types usually ignores such implementation issues as limited precision and word length. This limitation is less serious than it might at first appear: Checks for such errors as arithmetic overflow are usually implemented outside of the type system of a language anyway. They result in a run-time error, but this error is not called a type clash.
当程序员定义枚举类型(例如, C 语言中的enum hue {red, green, blue})时,他或她肯定会将此类型视为一组值。对于其他类型的用户定义类型,这种表示观点可能并不那么自然。相反,程序员可能会考虑类型是如何从更简单的类型构建而来的,或者考虑其含义或目的。这些思维方式分别反映了结构化和基于抽象的观点。结构化观点由 Algol W 和 Algol 68 率先提出,是 20 世纪 70 年代和 80 年代设计的许多语言的特征。基于抽象的观点由 Simula-67 和 Smalltalk 率先提出,是现代面向对象语言的特征;它也可以在其他各种语言的模块构造中找到,并且几乎可以在任何语言中作为编程规则采用。我们将在第 8 章中更详细地考虑结构化的观点,并在第 10 章中更详细地考虑基于抽象的观点。
When a programmer defines an enumerated type (e.g., enum hue {red, green, blue} in C), he or she certainly thinks of this type as a set of values. For other varieties of user-defined type, this denotational view may not be as natural. Instead, the programmer may think in terms of the way the type is built from simpler types, or in terms of its meaning or purpose. These ways of thinking reflect the structural and abstraction-based points of view, respectively. The structural point of view was pioneered by Algol W and Algol 68, and is characteristic of many languages designed in the 1970s and 1980s. The abstraction-based point of view was pioneered by Simula-67 and Smalltalk, and is characteristic of modern object-oriented languages; it can also be found in the module constructs of various other languages, and it can be adopted as a matter of programming discipline in almost any language. We will consider the structural point of view in more detail in Chapter 8, and the abstraction-based in Chapter 10.
多态性,我们在3.5.2 节中简要提到过,它的名字来源于希腊语,意思是“具有多种形式”。它适用于设计用于处理多种类型值的代码(数据结构和子程序)。为了保持正确性,类型通常必须具有某些共同的特征,并且代码不能依赖于任何其他特征。共性通常以两种主要方式之一捕获。在参数多态性中,代码以显式或隐式的方式将一个类型(或一组类型)作为参数。在子类型多态性中,代码设计用于处理某些特定类型T的值,但程序员可以定义其他类型作为T的扩展或细化,代码也可以使用这些子类型。
Polymorphism, which we mentioned briefly in Section 3.5.2, takes its name from the Greek, and means “having multiple forms.” It applies to code—both data structures and subroutines—that is designed to work with values of multiple types. To maintain correctness, the types must generally have certain characteristics in common, and the code must not depend on any other characteristics. The commonality is usually captured in one of two main ways. In parametric polymorphism the code takes a type (or set of types) as a parameter, either explicitly or implicitly. In subtype polymorphism, the code is designed to work with values of some specific type T, but the programmer can define additional types to be extensions or refinements of T, and the code will work with these subtypes as well.
显式参数多态性,也称为泛型(或C++ 中的模板),通常出现在静态类型语言中,通常在编译时实现。隐式版本也可以在编译时实现 - 具体来说,在 ML 系列语言中;更常见的是,它与动态类型配对,并在运行时进行检查。
Explicit parametric polymorphism, also known as generics (or templates in C++), typically appears in statically typed languages, and is usually implemented at compile time. The implicit version can also be implemented at compile time—specifically, in ML-family languages; more commonly, it is paired with dynamic typing, and the checking occurs at run time.
子类型多态性主要出现在面向对象语言中。使用静态类型,处理多种类型所需的大部分工作都可以在编译时执行:主要的运行时成本是方法调用的额外间接级别。大多数设想这种实现的语言,包括 C++、Eiffel、OCaml、Java 和 C#,都为泛型提供了单独的机制,也主要在编译时进行检查。子类型和参数多态性的组合对于容器(集合)类特别有用,例如“T 列表”(List<T> )或“ T堆栈”(Stack<T>),其中T最初未指定,稍后可以实例化为几乎任何类型。
Subtype polymorphism appears primarily in object-oriented languages. With static typing, most of the work required to deal with multiple types can be performed at compile time: the principal run-time cost is an extra level of indirection on method invocations. Most languages that envision such an implementation, including C++, Eiffel, OCaml, Java, and C#, provide a separate mechanism for generics, also checked mainly at compile time. The combination of subtype and parametric polymorphism is particularly useful for container (collection) classes such as “list of T” (List<T>) or “stack of T“ (Stack<T>), where T is initially unspecified, and can be instantiated later as almost any type.
相比之下,包括 Smalltalk、Python 和 Ruby 在内的动态类型面向对象语言通常使用单一机制来实现参数多态性和子类型多态性,并将检查延迟到运行时。Objective-C 中也出现了统一的机制,它在静态类型的基础上提供了动态类型对象。
By contrast, dynamically typed object-oriented languages, including Smalltalk, Python, and Ruby, generally use a single mechanism for both parametric and subtype polymorphism, with checking delayed until run time. A unified mechanism also appears in Objective-C, which provides dynamically typed objects on top of otherwise static typing.
在介绍完 ML 中的类型之后,我们将在第 7.3 节中更详细地讨论参数多态性。子类型多态性将主要推迟到第 10 章(介绍面向对象)和第 14.4.4 节(重点介绍脚本语言中的对象)。
We will consider parametric polymorphism in more detail in Section 7.3, after our coverage of typing in ML. Subtype polymorphism will largely be deferred to Chapter 10, which covers object orientation, and to Section 14.4.4, which focuses on objects in scripting languages.
作为正交性的另一个例子,考虑“擦除”变量值的常见需求——以表明它不包含其类型的有效值。对于指针类型,我们通常可以使用值null。对于枚举,我们可以在可能值集合中添加一个额外的“以上都不是”替代方案。但这两种技术非常不同,它们不能推广到已经利用了底层实现中所有可用位模式的类型。
As another example of orthogonality, consider the common need to “erase” the value of a variable—to indicate that it does not hold a valid value of its type. For pointer types, we can often use the value null. For enumerations, we can add an extra “none of the above” alternative to the set of possible values. But these two techniques are very different, and they don't generalize to types that already make use of all available bit patterns in the underlying implementation.
另一个正交性的例子是为复合类型的对象指定字面值。这种字面值有时被称为聚合。它们对于静态数据结构的初始化特别有用;如果没有它们,程序可能需要在运行时浪费时间进行初始化。
Yet another example of orthogonality arises when specifying literal values for objects of composite type. Such literals are sometimes known as aggregates. They are particularly valuable for the initialization of static data structures; without them, a program may need to waste time performing initialization at run time.
ML 提供了一种非常通用的复合表达式工具,它基于构造函数的使用(在11.4.3 节中讨论)。Lambda 表达式(我们在3.6.4 节中看到过,并将在第 11 章中再次讨论)相当于函数值的聚合。
ML provides a very general facility for composite expressions, based on the use of constructors (discussed in Section 11.4.3). Lambda expressions, which we saw in Section 3.6.4 and will discuss again in Chapter 11, amount to aggregates for values that are functions.
不同语言中类型的术语有所不同。本小节介绍最常见术语的定义。大多数语言都提供内置类型,类似于大多数处理器硬件支持的类型:整数、字符、布尔值和实数(浮点数)。
The terminology for types varies some from one language to another. This subsection presents definitions for the most common terms. Most languages provide built-in types similar to those supported in hardware by most processors: integers, characters, Booleans, and real (floating-point) numbers.
布尔值 (有时称为逻辑值) 通常实现为单字节量,其中1代表真,0代表假。在一些语言和实现中,布尔值可能打包成数组,每个值只使用一位。如第 6.1.2 节(“正交性”) 所述,C 在历史上不寻常地省略了布尔类型:大多数语言都期望布尔值,而 C 期望整数,使用零表示假,其他任何值表示真。C99 引入了一种新的 _Bool类型,但它实际上是一个整数,编译器被允许将其存储在单个位中。如第 C-6.5.4 节所述,Icon 用更通用的成功和失败概念取代了布尔值。
Booleans (sometimes called logicals) are typically implemented as single-byte quantities, with 1 representing true and 0 representing false. In a few languages and implementations, Booleans maybe packed into arrays using only one bit per value. As noted in Section 6.1.2 (“Orthogonality”), C was historically unusual in omitting a Boolean type: where most languages would expect a Boolean value, C expected an integer, using zero for false and anything else for true. C99 introduced a new _Bool type, but it is effectively an integer that the compiler is permitted to store in a single bit. As noted in Section C-6.5.4, Icon replaces Booleans with a more general notion of success and failure.
传统上,字符也是用单字节来实现的,通常(但不总是)使用 ASCII 编码。较新的语言(例如 Java 和 C#)使用双字节表示法来适应 Unicode 字符集(的常用部分)。Unicode是一种国际标准,旨在捕捉各种语言的字符(参见边栏 7.3)。Unicode 的前 128 个字符(\u0000到\u007f)与 ASCII 相同。C 和 C++ 提供常规字符和“宽”字符,但对于宽字符,编码和实际宽度都取决于实现。Fortran 2003 支持四字节 Unicode 字符。
Characters have traditionally been implemented as one-byte quantities as well, typically (but not always) using the ASCII encoding. More recent languages (e.g., Java and C#) use a two-byte representation designed to accommodate (the commonly used portion of) the Unicode character set. Unicode is an international standard designed to capture the characters of a wide variety of languages (see Sidebar 7.3). The first 128 characters of Unicode (\u0000 through \u007f) are identical to ASCII. C and C++ provide both regular and “wide” characters, though for wide characters both the encoding and the actual width are implementation dependent. Fortran 2003 supports four-byte Unicode characters.
少数语言(例如 C 和 Fortran)区分不同长度的整数和实数;大多数语言不区分,而是将精度的选择留给实现。不幸的是,不同语言实现之间的精度差异导致缺乏可移植性:在一个系统上正确运行的程序可能会在另一个系统上产生运行时错误或错误结果。Java 和 C# 不同寻常的是,它们提供了几种长度的数字类型,并且每种类型都有指定的精度。
A few languages (e.g., C and Fortran) distinguish between different lengths of integers and real numbers; most do not, and leave the choice of precision to the implementation. Unfortunately, differences in precision across language implementations lead to a lack of portability: programs that run correctly on one system may produce run-time errors or erroneous results on another. Java and C# are unusual in providing several lengths of numeric types, with a specified precision for each.
一些语言,包括 C、C++、C# 和 Modula-2,提供有符号和无符号整数(Modula-2 将无符号整数称为基数)。一些语言(例如 Fortran、C、Common Lisp 和 Scheme)提供内置复数类型,通常实现为一对表示实部和虚部笛卡尔坐标的浮点数;其他语言将其作为标准库类支持。一些语言(例如 Scheme 和 Common Lisp)提供内置有理数类型,通常实现为一对表示分子和分母的整数。大多数 Lisp 变体也支持任意精度的整数,大多数脚本语言也是如此;实现在适当的情况下使用多个内存字。Ada 支持定点类型,它们在内部由整数表示,但在程序员指定的数字位置处有一个隐含的小数点。几种语言支持十进制 使用十进制编码的类型来避免财务和以人为本的算术中的舍入异常(参见边栏 7.4)。
A few languages, including C, C++, C#, and Modula-2, provide both signed and unsigned integers (Modula-2 calls unsigned integers cardinals). A few languages (e.g., Fortran, C, Common Lisp, and Scheme) provide a built-in complex type, usually implemented as a pair of floating-point numbers that represent the real and imaginary Cartesian coordinates; other languages support these as a standard library class. A few languages (e.g., Scheme and Common Lisp) provide a built-in rational type, usually implemented as a pair of integers that represent the numerator and denominator. Most varieties of Lisp also support integers of arbitrary precision, as do most scripting languages; the implementation uses multiple words of memory where appropriate. Ada supports fixed-point types, which are represented internally by integers, but have an implied decimal point at a programmer-specified position among the digits. Several languages support decimal types that use a base-10 encoding to avoid round-off anomalies in financial and human-centered arithmetic (see Sidebar 7.4).
整数、布尔值和字符都是离散类型(也称为序数类型)的例子:它们对应的域是可数的(它们与整数的某个子集一一对应),并且对于除第一个和最后一个之外的每个元素都有明确定义的前任和后继概念。(在大多数实现中,可能的整数数量是有限的,但这通常不会反映在类型系统中。)两种用户定义类型,枚举和子范围,也是离散的。离散、有理、实数和复杂类型共同构成标量类型。标量类型有时也称为简单类型。
Integers, Booleans, and characters are all examples of discrete types (also called ordinal types): the domains to which they correspond are countable (they have a one-to-one correspondence with some subset of the integers), and have a well-defined notion of predecessor and successor for each element other than the first and the last. (In most implementations the number of possible integers is finite, but this is usually not reflected in the type system.) Two varieties of user-defined types, enumerations and subranges, are also discrete. Discrete, rational, real, and complex types together constitute the scalar types. Scalar types are also sometimes called simple types.
然而,在 Pascal 及其大多数后代中,枚举和一组整数常量之间的差异更为显著:枚举是一种成熟的类型,与整数不兼容。在期望使用整数或枚举值的上下文中使用另一个值将导致编译时出现类型冲突错误。
In Pascal and most of its descendants, however, the difference between an enumeration and a set of integer constants is much more significant: the enumeration is a full-fledged type, incompatible with integers. Using an integer or an enumeration value in a context expecting the other will result in a type clash error at compile time.
如第 3.5.2 节所述,Pascal 和 C 不允许在同一范围内的多个枚举类型中使用相同的元素名称。Java 和 C# 允许,但程序员必须使用完全限定名称来标识元素:arm_special_regs.fp。Ada放宽了这一要求,规定元素名称是重载的;只要编译器可以根据上下文推断出类型前缀,就可以省略它(示例 3.22)。C++ 在禁止重复枚举名称方面历来模仿 C。C++11 引入了一种新的枚举类型,它模仿 Java 和 C#(示例 3.23)。
As noted in Section 3.5.2, Pascal and C do not allow the same element name to be used in more than one enumeration type in the same scope. Java and C# do, but the programmer must identify elements using fully qualified names: arm_special_regs.fp. Ada relaxes this requirement by saying that element names are overloaded; the type prefix can be omitted whenever the compiler can infer it from context (Example 3.22). C++ historically mirrored C in prohibiting duplicate enum names. C++11 introduced a new variety of enum that mirrors Java and C# (Example 3.23).
当然,也可以用整数来表示测试分数,或者用工作日来表示工作日。使用显式子范围有几个优点。首先,它有助于记录程序。注释也可以用作文档,但是注释有一个坏习惯,就是随着程序的变化而变得过时,或者一开始就被省略。因为编译器会分析子范围声明,所以它知道子范围值的预期范围,并可以生成代码来执行动态语义检查,以确保没有子范围变量被赋予无效值。这些检查可以成为有价值的调试工具。此外,由于编译器知道子范围中值的数量,所以它有时可以使用比表示任意整数所需更少的位来表示子范围值。在上面的例子中,test_score值可以存储在一个字节中。
One could of course use integers to represent test scores, or a weekday to represent a workday. Using an explicit subrange has several advantages. For one thing, it helps to document the program. A comment could also serve as documentation, but comments have a bad habit of growing out of date as programs change, or of being omitted in the first place. Because the compiler analyzes a subrange declaration, it knows the expected range of subrange values, and can generate code to perform dynamic semantic checks to ensure that no subrange variable is ever assigned an invalid value. These checks can be valuable debugging tools. In addition, since the compiler knows the number of values in the subrange, it can sometimes use fewer bits to represent subrange values than it would need to use to represent arbitrary integers. In the example above, test_score values can be stored in a single byte.
非标量类型通常称为复合类型。它们通常是通过将类型构造函数应用于一个或多个更简单的类型来创建的。我们在示例 7.6中介绍的Options可以说是最简单的复合类型,其作用仅仅是向某个任意基类型的值添加额外的“以上都不是”。其他常见的复合类型包括记录(结构)、变体记录(联合)、数组、集合、指针、列表和文件。除了指针和列表之外,其他所有类型都可以轻松地用数学集合运算来描述(指针和列表也可以用数学方式描述,但描述不太直观)。
Nonscalar types are usually called composite types. They are generally created by applying a type constructor to one or more simpler types. Options, which we introduced in Example 7.6, are arguably the simplest composite types, serving only to add an extra “none of the above” to the values of some arbitrary base type. Other common composite types include records (structures), variant records (unions), arrays, sets, pointers, lists, and files. All but pointers and lists are easily described in terms of mathematical set operations (pointers and lists can be described mathematically as well, but the description is less intuitive).
记录(结构)由 Cobol 引入,自 20 世纪 60 年代以来,大多数语言都支持它。记录由字段集合组成,每个字段属于(可能不同的)更简单的类型。记录类似于数学元组;记录类型对应于字段类型的笛卡尔积。
Records (structs) were introduced by Cobol, and have been supported by most languages since the 1960s. A record consists of collection of fields, each of which belongs to a (potentially different) simpler type. Records are akin to mathematical tuples; a record type corresponds to the Cartesian product of the types of the fields.
变体记录(联合)与“普通”记录的不同之处在于,变体记录的字段(或字段集合)中只有一个在给定时间内有效。变体记录类型是其字段类型的不相交联合,而不是其笛卡尔积。
Variant records (unions) differ from “normal” records in that only one of a variant record's fields (or collections of fields) is valid at any given time. A variant record type is the disjoint union of its field types, rather than their Cartesian product.
数组是最常用的复合类型。数组可以被认为是将索引类型的成员映射到组件类型的成员的函数。字符数组通常称为字符串,并且通常由其他数组所不具备的特殊用途操作支持。
Arrays are the most commonly used composite types. An array can be thought of as a function that maps members of an index type to members of a component type. Arrays of characters are often referred to as strings, and are often supported by special-purpose operations not available for other arrays.
集合,与枚举和子范围一样,是由 Pascal 引入的。集合类型是其基类型的数学幂集,通常必须是离散的。集合类型的变量包含基类型的不同元素的集合。
Sets, like enumerations and subranges, were introduced by Pascal. A set type is the mathematical powerset of its base type, which must often be discrete. A variable of a set type contains a collection of distinct elements of the base type.
指针是左值。指针值是对指针基类型的对象的引用。指针通常(但并非总是)实现为地址。它们最常用于实现递归数据类型。如果类型T的对象可能包含一个或多个对类型T的其他对象的引用,则类型T是递归的。
Pointers are l-values. A pointer value is a reference to an object of the pointer's base type. Pointers are often but not always implemented as addresses. They are most often used to implement recursive data types. A type T is recursive if an object of type T may contain one or more references to other objects of type T.
列表与数组一样,包含元素序列,但没有映射或索引的概念。相反,列表以递归方式定义为空列表或由头元素和对子列表的引用组成的对。虽然大多数(但不是全部)语言都必须在阐述时指定数组的长度,但列表的长度始终是可变的。要找到列表中的给定元素,程序必须从头部开始递归或迭代地检查所有先前的元素。由于列表具有递归定义,因此列表是大多数函数式语言编程的基础。
Lists, like arrays, contain a sequence of elements, but there is no notion of mapping or indexing. Rather, a list is defined recursively as either an empty list or a pair consisting of a head element and a reference to a sublist. While the length of an array must be specified at elaboration time in most (though not all) languages, lists are always of variable length. To find a given element of a list, a program must examine all previous elements, recursively or iteratively, starting at the head. Because of their recursive definition, lists are fundamental to programming in most functional languages.
文件用于表示大容量存储设备上的数据,位于其他程序对象所在的内存之外。与数组一样,大多数文件可以概念化为将索引类型(通常是整数)的成员映射到组件类型的成员的函数。与数组不同,文件通常具有当前位置的概念,这允许在连续操作中隐式地暗示索引。文件通常会显示从物理输入/输出设备继承的特性。特别是,某些文件的元素必须按顺序访问。
Files are intended to represent data on mass-storage devices, outside the memory in which other program objects reside. Like arrays, most files can be conceptualized as a function that maps members of an index type (generally integer) to members of a component type. Unlike arrays, files usually have a notion of current position, which allows the index to be implied implicitly in consecutive operations. Files often display idiosyncrasies inherited from physical input/output devices. In particular, the elements of some files must be accessed in sequential order.
我们将在第 8 章中更详细地讨论复合类型。
We will examine composite types in more detail in Chapter 8.
在大多数静态类型语言中,对象(常量、变量、子程序等)的每个定义都必须指定对象的类型。此外,对象可能出现的许多上下文也是有类型的,从某种意义上说,语言规则限制了该上下文中的对象可以有效拥有的类型。在下面的小节中,我们将考虑类型等价、类型兼容性和类型推断的主题。在这三者中,类型兼容性是程序员最关心的。它决定了何时可以在某个上下文中使用某种类型的对象。至少,如果对象的类型和上下文期望的类型是等价的(即相同),则可以使用该对象。然而,在许多语言中,兼容性是一种比等价更松散的关系:对象和上下文往往兼容,即使它们的类型不同。我们对类型兼容性的讨论将涉及类型转换(也称为强制类型转换),它将一种类型的值转换为另一种类型的值;类型强制,在特定上下文中自动执行转换;非转换类型转换,有时在系统编程中使用,将一种类型的值的位解释为好像它们表示其他类型的值。
In most statically typed languages, every definition of an object (constant, variable, subroutine, etc.) must specify the object's type. Moreover, many of the contexts in which an object might appear are also typed, in the sense that the rules of the language constrain the types that an object in that context may validly possess. In the subsections below we will consider the topics of type equivalence, type compatibility, and type inference. Of the three, type compatibility is the one of most concern to programmers. It determines when an object of a certain type can be used in a certain context. At a minimum, the object can be used if its type and the type expected by the context are equivalent (i.e., the same). In many languages, however, compatibility is a looser relationship than equivalence: objects and contexts are often compatible even when their types are different. Our discussion of type compatibility will touch on the subjects of type conversion (also called casting), which changes a value of one type into a value of another; type coercion, which performs a conversion automatically in certain contexts; and nonconverting type casts, which are sometimes used in systems programming to interpret the bits of a value of one type as if they represented a value of some other type.
每当一个表达式由更简单的子表达式构造时,就会出现一个问题:给定子表达式的类型(以及可能期望的类型),如果表达式的类型是随机的(即上下文),那么整个表达式的类型是什么?类型推断可以回答这个问题。类型推断通常很简单:例如,两个整数之和仍然是整数。在其他情况下(例如,处理集合时),它会更加棘手。类型推断在 ML、Miranda 和 Haskell 中起着特别重要的作用,在这些语言中,几乎所有类型注释都是可选的,如果省略,编译器将推断类型。
Whenever an expression is constructed from simpler subexpressions, the question arises: given the types of the subexpressions (and possibly the type expected by the surrounding context), what is the type of the expression as a whole? This question is answered by type inference. Type inference is often trivial: the sum of two integers is still an integer, for example. In other cases (e.g., when dealing with sets) it is a good bit trickier. Type inference plays a particularly important role in ML, Miranda, and Haskell, in which almost all type annotations are optional, and will be inferred by the compiler when omitted.
在用户可以定义新类型的语言中,定义类型等价有两种主要方式。结构等价基于类型定义的内容:粗略地说,如果两种类型由相同的组件组成,并且以相同的方式组合在一起,则它们是相同的。名称等价基于类型定义的词汇出现:粗略地说,每个定义都会引入一种新类型。结构等价用于 Algol-68、Modula-3 以及(有各种不同之处的)C 和 ML。名称等价出现在 Java、C#、标准 Pascal 和大多数 Pascal 后代(包括 Ada)中。
In a language in which the user can define new types, there are two principal ways of defining type equivalence. Structural equivalence is based on the content of type definitions: roughly speaking, two types are the same if they consist of the same components, put together in the same way. Name equivalence is based on the lexical occurrence of type definitions: roughly speaking, each definition introduces a new type. Structural equivalence is used in Algol-68, Modula-3, and (with various wrinkles) C and ML. Name equivalence appears in Java, C#, standard Pascal, and most Pascal descendants, including Ada.
要确定两种类型在结构上是否等效,编译器可以通过用其各自的定义替换任何嵌入的类型名称来扩展它们的定义,递归地进行,直到只剩下一长串类型构造函数、字段名称和内置类型。如果这些扩展的字符串相同,则类型等效,反之亦然。递归和基于指针的类型使问题变得复杂,因为它们的扩展不会终止,但问题并非无法克服;我们在练习 8.15中考虑了一个解决方案。
To determine if two types are structurally equivalent, a compiler can expand their definitions by replacing any embedded type names with their respective definitions, recursively, until nothing is left but a long string of type constructors, field names, and built-in types. If these expanded strings are the same, then the types are equivalent, and conversely. Recursive and pointer-based types complicate matters, since their expansion does not terminate, but the problem is not insurmountable; we consider a solution in Exercise 8.15.
理解严格名称等价和宽松名称等价之间的区别的一种方法是记住声明和定义之间的区别(第 3.3.3 节)。在严格名称等价下,声明类型 A = B被视为定义。在宽松名称等价下,它仅仅是一个声明;A共享B的定义。
One way to think about the difference between strict and loose name equivalence is to remember the distinction between declarations and definitions (Section 3.3.3). Under strict name equivalence, a declaration type A = B is considered a definition. Under loose name equivalence it is merely a declaration; A shares the definition of B.
在单独编译的情况下,结构等价和名称等价都很难实现。我们将在15.6 节中讨论这个问题。
Both structural and name equivalence can be tricky to implement in the presence of separate compilation. We will return to this issue in Section 15.6.
假设我们目前要求在每种情况下类型(预期和提供)完全相同。那么,如果程序员希望在需要另一种类型的上下文中使用一种类型的值,他或她将需要指定显式类型转换(有时也称为类型转换)。根据所涉及的类型,转换可能需要或不需要在运行时执行代码。主要有三种情况:
Suppose for the moment that we require in each of these cases that the types (expected and provided) be exactly the same. Then if the programmer wishes to use a value of one type in a context that expects another, he or she will need to specify an explicit type conversion (also sometimes called a type cast). Depending on the types involved, the conversion may or may not require code to be executed at run time. There are three principal cases:
1. 类型在结构上被视为等价,但语言使用名称等价。在这种情况下,类型采用相同的低级表示,并具有相同的值集。因此,转换是纯粹的概念操作;运行时不需要执行任何代码。
1. The types would be considered structurally equivalent, but the language uses name equivalence. In this case the types employ the same low-level representation, and have the same set of values. The conversion is therefore a purely conceptual operation; no code will need to be executed at run time.
2. 类型具有不同的值集,但相交值的表示方式相同。例如,一种类型可能是另一种类型的子范围,或者一种类型可能由二进制补码有符号整数组成,而另一种类型是无符号的。如果提供的类型具有预期类型所没有的一些值,则必须在运行时执行代码以确保当前值在预期类型中有效。如果检查失败,则会导致动态语义错误。如果检查成功,则可以使用该值的底层表示,无需更改。某些语言实现可能允许禁用检查,从而导致速度更快但可能不安全的代码。
2. The types have different sets of values, but the intersecting values are represented in the same way. One type may be a subrange of the other, for example, or one may consist of two's complement signed integers, while the other is unsigned. If the provided type has some values that the expected type does not, then code must be executed at run time to ensure that the current value is among those that are valid in the expected type. If the check fails, then a dynamic semantic error results. If the check succeeds, then the underlying representation of the value can be used, unchanged. Some language implementations may allow the check to be disabled, resulting in faster but potentially unsafe code.
3. 类型具有不同的低级表示,但我们仍然可以定义它们的值之间的某种对应关系。例如,32 位整数可以转换为双精度 IEEE 浮点数,而不会损失精度。大多数处理器都提供了机器指令来实现此转换。浮点数可以通过四舍五入或截断转换为整数,但小数位会丢失,并且转换将溢出许多指数值。同样,大多数处理器都提供了机器指令来实现此转换。不同长度的整数之间的转换可以通过丢弃或符号扩展高位字节来实现。
3. The types have different low-level representations, but we can nonetheless define some sort of correspondence among their values. A 32-bit integer, for example, can be converted to a double-precision IEEE floating-point number with no loss of precision. Most processors provide a machine instruction to effect this conversion. A floating-point number can be converted to an integer by rounding or truncating, but fractional digits will be lost, and the conversion will overflow for many exponent values. Again, most processors provide a machine instruction to effect this conversion. Conversions between different lengths of integers can be effected by discarding or sign-extending high-order bytes.
有时,特别是在系统程序中,需要更改值的类型而不更改底层实现;换句话说,将一种类型的值的位解释为另一种类型。一个常见的例子出现在内存分配算法中,该算法使用大量字节数组来表示堆,然后将该数组的部分重新解释为指针和整数(用于簿记目的),或各种用户分配的数据结构。另一个常见的例子出现在高性能数字软件中,它可能需要将浮点数重新解释为整数或记录,以便提取指数、有效数字和符号字段。这些字段可用于实现平方根、三角函数等专用算法。
Occasionally, particularly in systems programs, one needs to change the type of a value without changing the underlying implementation; in other words, to interpret the bits of a value of one type as if they were another type. One common example occurs in memory allocation algorithms, which use a large array of bytes to represent a heap, and then reinterpret portions of that array as pointers and integers (for bookkeeping purposes), or as various user-allocated data structures. Another common example occurs in high-performance numeric software, which may need to reinterpret a floating-point number as an integer or a record, in order to extract the exponent, significand, and sign fields. These fields can be used to implement special-purpose algorithms for square root, trigonometric functions, and so on.
任何非转换类型转换都会对语言的类型系统造成危险的破坏。在具有弱类型系统的语言中,这种破坏可能很难发现。在具有强类型系统的语言中,使用显式非转换类型转换至少会标记代码中的危险点,从而便于在出现问题时进行调试。
Any nonconverting type cast constitutes a dangerous subversion of the language's type system. In a language with a weak type system such subversions can be difficult to find. In a language with a strong type system, the use of explicit nonconverting type casts at least labels the dangerous points in the code, facilitating debugging if problems arise.
大多数语言并不要求在每种情况下类型都相等。相反,它们只是说值的类型必须与其出现的上下文的类型兼容。在赋值语句中,右侧的类型必须与左侧的类型兼容。+ 的操作数的类型必须都与支持加法的某些常见类型兼容(整数、实数,或者可能是字符串或集合)。在子程序调用中,传递到子程序中的任何参数的类型都必须与相应形式参数的类型兼容,并且传递回调用者的任何形式参数的类型都必须与相应参数的类型兼容。
Most languages do not require equivalence of types in every context. Instead, they merely say that a value's type must be compatible with that of the context in which it appears. In an assignment statement, the type of the right-hand side must be compatible with that of the left-hand side. The types of the operands of + must both be compatible with some common type that supports addition (integers, real numbers, or perhaps strings or sets). In a subroutine call, the types of any arguments passed into the subroutine must be compatible with the types of the corresponding formal parameters, and the types of any formal parameters passed back to the caller must be compatible with the types of the corresponding arguments.
类型兼容性的定义在不同语言之间差别很大。Ada 采用了相对严格的方法:当且仅当 (1) S和T等价、(2) 一个是另一个的子类型(或两者都是同一基类型的子类型)或 (3) 两者都是数组,且每个维度的元素数量和类型相同时, Ada 类型S与预期类型 T 兼容。Pascal 只是稍微宽松一点:除了允许混合使用基类型和子范围类型之外,它还允许在需要实数的上下文中使用整数。
The definition of type compatibility varies greatly from language to language. Ada takes a relatively restrictive approach: an Ada type S is compatible with an expected type T if and only if (1) S and T are equivalent, (2) one is a subtype of the other (or both are subtypes of the same base type), or (3) both are arrays, with the same numbers and types of elements in each dimension. Pascal was only slightly more lenient: in addition to allowing the intermixing of base and subrange types, it allowed an integer to be used in a context where a real was expected.
每当一种语言允许在需要另一种类型的上下文中使用一种类型的值时,该语言实现必须执行自动、隐式转换为预期类型。此转换称为类型强制。与第 7.2.1 节的显式转换一样,强制可能需要运行时代码执行动态语义检查或在低级表示之间进行转换。
Whenever a language allows a value of one type to be used in a context that expects another, the language implementation must perform an automatic, implicit conversion to the expected type. This conversion is called a type coercion. Like the explicit conversions of Section 7.2.1, coercion may require run-time code to perform a dynamic semantic check or to convert between low-level representations.
强制转换在语言设计中是一个颇具争议的话题。由于它允许混合使用不同类型,而程序员对此没有明确的意图,因此它严重削弱了类型的安全性。与此同时,一些设计者认为强制转换是支持抽象和程序可扩展性的自然方式,因为它使新类型与现有类型的结合使用变得更加容易。这种可扩展性论点在脚本语言(第14 章)中尤其引人注目,因为脚本语言是动态类型的,并且强调编程的简易性。大多数脚本语言都支持各种各样的强制转换,但也存在一些差异:Perl 几乎可以强制转换所有类型;而 Ruby 则要保守得多。
Coercion is a somewhat controversial subject in language design. Because it allows types to be mixed without an explicit indication of intent on the part of the programmer, it represents a significant weakening of type security. At the same time, some designers have argued that coercions are a natural way in which to support abstraction and program extensibility, by making it easier to use new types in conjunction with existing ones. This extensibility argument is particularly compelling in scripting languages (Chapter 14), which are dynamically typed and emphasize ease of programming. Most scripting languages support a wide variety of coercions, though there is some variation: Perl will coerce almost anything; Ruby is much more conservative.
在静态类型语言中,种类更多。Ada 只强制转换显式常量、子范围,以及在某些情况下强制转换具有相同类型元素的数组。Pascal 会在表达式和赋值中将整数强制转换为浮点数。Fortran 还会在赋值中将浮点值强制转换为整数,但可能会损失精度。C 会对函数的参数执行相同的强制转换。
Among statically typed languages, there is even more variety. Ada coerces nothing but explicit constants, subranges, and in certain cases arrays with the same type of elements. Pascal would coerce integers to floating-point in expressions and assignments. Fortran will also coerce floating-point values to integers in assignments, at a potential loss of precision. C will perform these same coercions on arguments to functions.
一些编译语言甚至支持对数组和记录进行强制转换。只要预期类型和实际类型具有相同的形状,Fortran 90 就允许这样做。如果两个数组具有相同数量的维数、每个维度具有相同的大小(即相同数量的元素)并且各个元素具有相同的形状,则它们具有相同的形状。如果两个记录具有相同数量的字段,并且相应的字段按顺序具有相同的形状,则它们具有相同的形状。字段名称并不重要,数组维数的实际上限和下限也不重要。Ada 的数组强制转换规则大致相当于 Fortran 90 的规则。C 不提供将整个数组作为操作数的运算。然而,C 在许多情况下允许数组和指针混合使用;我们将在第 8.5.1 节中进一步讨论这种不寻常的类型兼容性形式。Ada 和 C 都不允许记录(结构)混合使用,除非它们的类型是名称等效的。
Some compiled languages even support coercion on arrays and records. Fortran 90 permits this whenever the expected and actual types have the same shape. Two arrays have the same shape if they have the same number of dimensions, each dimension has the same size (i.e., the same number of elements), and the individual elements have the same shape. Two records have the same shape if they have the same number of fields, and corresponding fields, in order, have the same shape. Field names do not matter, nor do the actual high and low bounds of array dimensions. Ada's coercion rules for arrays are roughly equivalent to those of Fortran 90. C provides no operations that take an entire array as an operand. C does, however, allow arrays and pointers to be intermixed in many cases; we will discuss this unusual form of type compatibility further in Section 8.5.1. Neither Ada nor C allows records (structures) to be intermixed unless their types are name equivalent.
C++ 提供了静态类型语言中强制转换的最极端示例。除了一组丰富的内置规则外,C++ 还允许程序员在定义新类型(类)时定义与现有类型之间的强制转换操作。应用这些操作的规则与解决重载的规则(第 3.5.2 节)以复杂的方式相互作用;它们为语言增加了很大的灵活性,但却是最难理解和正确使用的 C++ 特性之一。
C++ provides what may be the most extreme example of coercion in a statically typed language. In addition to a rich set of built-in rules, C++ allows the programmer to define coercion operations to and from existing types when defining a new type (a class). The rules for applying these operations interact in complicated ways with the rules for resolving overloading (Section 3.5.2); they add significant flexibility to the language, but are one of the most difficult C++ features to understand and use correctly.
在大多数语言中,文字常量(例如数字、字符串、空集 [ [ ] ] 或空指针 [ nil ])可以在表达式中与多种类型的值混合使用。 有人可能会说常量是重载的:例如nil可能被认为是引用周围上下文中所需的任何类型的空指针值。 但更常见的是,常量只是被视为语言类型检查规则中的特例。 在内部,编译器将常量视为少数内置“常量类型”之一(int const、real const、string、null),然后根据需要将其强制转换为某种更合适的类型,即使该语言的其他地方不支持强制转换。 Ada 将这种“常量类型”概念形式化为数值:整型常量(没有小数点)被称为具有universal_integer类型;实数常量(带有嵌入小数点和/或指数的常量)的类型为universal_real。universal_integer类型与任何整数类型兼容;universal_real与任何定点或浮点类型兼容。
In most languages, literal constants (e.g., numbers, character strings, the empty set [ [ ] ] or the null pointer [nil]) can be intermixed in expressions with values of many types. One might say that constants are overloaded: nil for example might be thought of as referring to the null pointer value for whatever type is needed in the surrounding context. More commonly, however, constants are simply treated as a special case in the language's type-checking rules. Internally, the compiler considers a constant to have one of a small number of built-in “constant types” (int const, real const, string, null), which it then coerces to some more appropriate type as necessary, even if coercions are not supported elsewhere in the language. Ada formalizes this notion of “constant type” for numeric quantities: an integer constant (one without a decimal point) is said to have type universal_integer; a real-number constant (one with an embedded decimal point and/or an exponent) is said to have type universal_real. The universal_integer type is compatible with any integer type; universal_real is compatible with any fixed-point or floating-point type.
对于系统编程,或为了方便编写保存对其他对象的引用的通用容器(集合)对象(列表、堆栈、队列、集合等),有几种语言提供了通用引用类型。在 C 和 C++ 中,这种类型称为void *。在 Clu 中,它被称为any;在 Modula-2 中,称为address;在 Modula-3 中,称为refany;在 Java 中,称为Object;在 C# 中,称为object。可以将任意左值分配给通用引用类型的对象,而不必担心类型安全:因为通用引用所引用的对象类型未知,所以编译器不允许对该对象执行任何操作。如果要维护类型安全,将值分配回特定引用类型的对象(例如,指向程序员指定的记录类型的指针)会有些棘手。例如,我们不希望将对浮点数的通用引用赋值给一个应该保存对整数的引用的变量,因为对“整数”的后续操作会错误地解释对象的位。在面向对象语言中,如何确保通用到特定赋值的有效性的问题可以推广到如何确保任何赋值的有效性的问题,其中左侧对象的类型支持右侧对象可能不支持的操作。
For systems programming, or to facilitate the writing of general-purpose container (collection) objects (lists, stacks, queues, sets, etc.) that hold references to other objects, several languages provide a universal reference type. In C and C++, this type is called void*. In Clu it is called any; in Modula-2, address; in Modula-3, refany; in Java, Object; in C#, object. Arbitrary l-values can be assigned into an object of universal reference type, with no concern about type safety: because the type of the object referred to by a universal reference is unknown, the compiler will not allow any operations to be performed on that object. Assignments back into objects of a particular reference type (e.g., a pointer to a programmer-specified record type) are a bit trickier, if type safety is to be maintained. We would not want a universal reference to a floating-point number, for example, to be assigned into a variable that is supposed to hold a reference to an integer, because subsequent operations on the “integer” would interpret the bits of the object incorrectly. In object-oriented languages, the question of how to ensure the validity of a universal-to-specific assignment generalizes to the question of how to ensure the validity of any assignment in which the type of the object on left-hand side supports operations that the object on the right-hand side may not.
确保通用到特定赋值(或者,一般来说,从不太特定到更特定的赋值)安全性的一种方法是使对象具有自描述性 - 即,在每个对象的表示中包含一个指示其类型的标签。 这种方法在面向对象语言中很常见,通常需要它进行动态方法绑定。 对象中的类型标签会占用大量空间,但允许实现防止将一种类型的对象赋值给另一种类型的变量。 在 Java 和 C# 中,通用到特定赋值需要类型转换,如果通用引用不引用转换类型的对象,则会生成异常。 在 Eiffel 中,等效操作使用特殊的赋值运算符(?=而不是:=);在 C++ 中,它使用dynamic_cast操作。
One way to ensure the safety of universal to specific assignments (or, in general, less specific to more specific assignments) is to make objects self-descriptive—that is, to include in the representation of each object a tag that indicates its type. This approach is common in object-oriented languages, which generally need it for dynamic method binding. Type tags in objects can consume a nontrivial amount of space, but allow the implementation to prevent the assignment of an object of one type into a variable of another. In Java and C#, a universal to specific assignment requires a type cast, and will generate an exception if the universal reference does not refer to an object of the casted type. In Eiffel, the equivalent operation uses a special assignment operator (?= instead of :=); in C++ it uses a dynamic_cast operation.
在没有类型标签的语言中,无法检查将通用引用分配给特定引用类型的对象,因为对象不是自描述的:无法在运行时识别其类型。因此,程序员必须求助于(未经检查的)类型转换。
In a language without type tags, the assignment of a universal reference into an object of a specific reference type cannot be checked, because objects are not self-descriptive: there is no way to identify their type at run time. The programmer must therefore resort to an (unchecked) type conversion.
我们已经了解了类型检查如何确保表达式的组成部分(例如,二元运算符的参数)具有适当的类型。但是,是什么决定了整个表达式的类型呢?在许多情况下,答案很简单。算术运算符的结果通常与操作数具有相同的类型(如果它们的类型不同,则可能在强制其中一个操作数之后)。比较的结果通常是布尔值。函数调用的结果具有在函数头中声明的类型。赋值的结果(在赋值是表达式的语言中)具有与左侧相同的类型。然而,在少数情况下,答案并不明显。例如,对子范围和复合对象的操作不一定保留操作数的类型。我们将在本小节的其余部分中研究这些情况。在下一节中,我们将考虑在 ML、Miranda 和 Haskell 中发现的一种更复杂的类型推断形式。
We have seen how type checking ensures that the components of an expression (e.g., the arguments of a binary operator) have appropriate types. But what determines the type of the overall expression? In many cases, the answer is easy. The result of an arithmetic operator usually has the same type as the operands (possibly after coercing one of them, if their types were not the same). The result of a comparison is usually Boolean. The result of a function call has the type declared in the function's header. The result of an assignment (in languages in which assignments are expressions) has the same type as the left-hand side. In a few cases, however, the answer is not obvious. Operations on subranges and composite objects, for example, do not necessarily preserve the types of the operands. We examine these cases in the remainder of this subsection. In the following section, we consider a more elaborate form of type inference found in ML, Miranda, and Haskell.
Ada 是第一批将for循环的索引设为新的局部变量(仅在循环中可访问)的语言之一。该语言无需程序员指定此变量的类型,而是隐式地为其分配了作为循环界限提供的表达式的基类型。
Ada was among the first languages to make the index of a for loop a new, local variable, accessible only in the loop. Rather than require the programmer to specify the type of this variable, the language implicitly assigned it the base type of the expressions provided as bounds for the loop.
类型推断的最复杂形式出现在 ML 系列函数式语言中,包括 Haskell、F# 以及 ML 本身的 OCaml 和 SML 方言。程序员可以选择在这些语言中声明对象的类型,在这种情况下,编译器的行为与更传统的静态类型语言非常相似。然而,正如我们在第7.1 节开头提到的那样,程序员也可以选择不声明某些类型,在这种情况下,编译器将根据已知的文字常量类型、具有它们的任何对象的显式声明类型以及语法结构推断它们程序。ML 样式类型推断是该语言的创建者 Robin Milner 的发明。4
The most sophisticated form of type inference occurs in the ML family of functional languages, including Haskell, F#, and the OCaml and SML dialects of ML itself. Programmers have the option of declaring the types of objects in these languages, in which case the compiler behaves much like that of a more traditional statically typed language. As we noted near the beginning of Section 7.1, however, programmers may also choose not to declare certain types, in which case the compiler will infer them, based on the known types of literal constants, the explicitly declared types of any objects that have them, and the syntactic structure of the program. ML-style type inference is the invention of the language's creator, Robin Milner.4
推理机制的关键是,当类型系统的规则规定两个表达式的类型必须相同时,统一它们的(部分)类型信息。已知的彼此信息也会知道对方的信息。任何发现的不一致之处都被标识为静态语义错误。任何在推理后类型仍未完全指定的表达式都会自动成为多态的;这就是第7.1.2 节中提到的隐式参数多态性。ML 系列语言还包含强大的运行时模式匹配功能和几种非常规的结构化类型,包括有序元组、(无序)记录、列表、包含联合和递归类型的数据类型机制,以及具有继承(类型扩展)和显式参数多态性(泛型)的丰富模块系统。我们将在第11.4 节中更详细地讨论 ML 类型。
The key to the inference mechanism is to unify the (partial) type information available for two expressions whenever the rules of the type system say that their types must be the same. Information known about each is then known about the other as well. Any discovered inconsistencies are identified as static semantic errors. Any expression whose type remains incompletely specified after inference is automatically polymorphic; this is the implicit parametric polymorphism referred to in Section 7.1.2. ML family languages also incorporate a powerful run-time pattern-matching facility and several unconventional structured types, including ordered tuples, (unordered) records, lists, a datatype mechanism that subsumes unions and recursive types, and a rich module system with inheritance (type extension) and explicit parametric polymorphism (generics). We will consider ML types in more detail in Section 11.4.
尽管该语言通常在生产环境中编译,但标准 OCaml 发行版还包含一个交互式解释器。程序员可以“在线”与解释器交互,每次输入一行。解释器会逐步处理这些输入,为每个源代码函数生成中间表示,并生成任何适当的静态错误消息。这种交互方式模糊了解释和编译之间的传统区别。虽然语言实现在程序执行期间保持活动状态,但它会在评估给定的程序片段之前执行所有可能的语义检查(生产编译器会检查的所有内容)。
Though the language is usually compiled in production environments, the standard OCaml distribution also includes an interactive interpreter. The programmer can interact with the interpreter “on line,” giving it input a line at a time. The interpreter processes this input incrementally, generating an intermediate representation for each source code function, and producing any appropriate static error messages. This style of interaction blurs the traditional distinction between interpretation and compilation. While the language implementation remains active during program execution, it performs all possible semantic checks—everything that the production compiler would check—before evaluating a given program fragment.
OCaml 编译器根据一组明确定义的约束来验证类型一致性。例如,
An OCaml compiler verifies type consistency with respect to a well-defined set of constraints. For example,
■ All occurrences of the same identifier (subject to scope rules) have the same type.
■ 在if…then…else表达式中,条件为bool类型,并且then和else子句具有相同的类型。
■ In an if… then … else expression, the condition is of type bool, and the then and else clauses have the same type.
■ 程序员定义的函数具有类型'a -> 'b -> …-> 'r,其中'a、'b等是该函数参数的类型,'r是其结果(构成其主体的表达式)的类型。
■ A programmer-defined function has type 'a -> 'b -> …-> 'r, where 'a, 'b, and so forth are the types of the function's parameters, and 'r is the type of its result (the expression that forms its body).
■ 当应用(调用)函数时,传递的实参的类型与函数定义中的形参的类型相同。应用(即调用构成的表达式)的类型与函数定义中结果的类型相同。
■ When a function is applied (called), the types of the arguments that are passed are the same as the types of the parameters in the function's definition. The type of the application (i.e., the expression constituted by the call) is the same as the type of the result in the function's definition.
这种检查操作风格(如果对象支持请求的方法,则该对象具有可接受的类型)有时称为鸭子类型。它的名字来源于“如果它走路像鸭子,叫声像鸭子,那么它一定是鸭子。” 6
This operational style of checking (an object has an acceptable type if it supports the requested method) is sometimes known as duck typing. It takes its name from the notion that “if it walks like a duck and quacks like a duck, then it must be a duck.”6
Scheme、Smalltalk、Ruby 等语言中的多态性缺点是需要进行运行时检查,这会产生不小的成本,并延迟错误报告。ML 系列语言的隐式多态性避免了这些缺点,但需要高级类型推断。对于其他编译语言,显式参数多态性(也称为泛型)允许程序员在声明子例程或类时指定类型参数。然后,编译器在静态类型检查过程中使用这些参数。
The disadvantage of polymorphism in Scheme, Smalltalk, Ruby, and the like is the need for run-time checking, which incurs nontrivial costs, and delays the reporting of errors. The implicit polymorphism of ML-family languages avoids these disadvantages, but requires advanced type inference. For other compiled languages, explicit parametric polymorphism (otherwise known as generics) allows the programmer to specify type parameters when declaring a subroutine or class. The compiler then uses these parameters in the course of static type checking.
泛型可以通过多种方式实现。在 Ada 和 C++ 的大多数实现中,它们是一种纯静态机制:创建和使用泛型代码的多个实例所需的所有工作都在编译时进行。通常情况下,编译器会为每个实例创建一份单独的代码副本。(C++更进一步,并安排对这些实例中的每一个进行独立类型检查。)如果使用同一组参数实例化多个队列,则编译器可能会在它们之间共享入队和出队例程的代码。如果两种类型恰好具有相同的大小,那么聪明的编译器可能会安排将整数队列的代码与浮点数队列的代码共享,但这种优化不是必需的,如果没有发生这种情况,程序员也不应该感到惊讶。
Generics can be implemented several ways. In most implementations of Ada and C++ they are a purely static mechanism: all the work required to create and use multiple instances of the generic code takes place at compile time. In the usual case, the compiler creates a separate copy of the code for every instance. (C++ goes farther, and arranges to type-check each of these instances independently.) If several queues are instantiated with the same set of arguments, then the compiler may share the code of the enqueue and dequeue routines among them. A clever compiler may arrange to share the code for a queue of integers with the code for a queue of floating-point numbers, if the two types happen to have the same size, but this sort of optimization is not required, and the programmer should not be surprised if it doesn't occur.
相比之下,Java 保证给定泛型的所有实例在运行时共享相同的代码。实际上,如果T是 Java 中的泛型类型参数,则类T的对象将被视为标准基类Object的实例,只是程序员不必插入显式强制类型转换即可将它们用作类T的对象,并且编译器静态保证省略的强制类型转换永远不会失败。C# 走了一条折衷路线。与 C++ 一样,它将为不同的原始类型或值类型创建泛型的专门实现。但是,与 Java 一样,它要求泛型代码本身明显是类型安全的,与任何特定实例中提供的参数无关。我们将在C-7.3.2 节中更详细地研究 C++、Java 和 C# 泛型之间的权衡。
Java, by contrast, guarantees that all instances of a given generic will share the same code at run time. In effect, if T is a generic type parameter in Java, then objects of class T are treated as instances of the standard base class Object, except that the programmer does not have to insert explicit casts to use them as objects of class T, and the compiler guarantees, statically, that the elided casts will never fail. C# plots an intermediate course. Like C++, it will create specialized implementations of a generic for different primitive or value types. Like Java, however, it requires that the generic code itself be demonstrably type safe, independent of the arguments provided in any particular instantiation. We will examine the tradeoffs among C++, Java, and C# generics in more detail in Section C-7.3.2.
因为泛型是一种抽象,所以它的接口(声明的标题)提供抽象用户必须知道的所有信息非常重要。包括 Ada、Java、C#、Scala、OCaml 和 SML 在内的多种语言都支持这种接口。尝试通过约束泛型参数来强制执行此规则。具体来说,它们要求显式声明允许对泛型参数类型执行的操作。
Because a generic is an abstraction, it is important that its interface (the header of its declaration) provide all the information that must be known by a user of the abstraction. Several languages, including Ada, Java, C#, Scala, OCaml, and SML, attempt to enforce this rule by constraining generic parameters. Specifically, they require that the operations permitted on a generic parameter type be explicitly declared.
图 7.4总结了 Ada、C++、Java 和 C# 泛型的特性,以及 Lisp 和 ML 的隐式参数多态性。部分细节的进一步解释见第 C-7.3.2 节。
Figure 7.4 summarizes the features of Ada, C++, Java, and C# generics, and of the implicit parametric polymorphism of Lisp and ML. Further explanation of some of the details appears in Section C-7.3.2.
通过比较 C++、Java 和 C# 的特性,可以说明泛型设计中的几个关键权衡。C++ 是这三个语言中最具雄心的。它的模板适用于几乎所有需要基本相似但不完全相同的抽象副本的编程任务。Java 和 C# 提供泛型纯粹是为了实现多态性。Java 的设计深受向后兼容需求的影响,不仅与现有版本的语言兼容,而且与现有的虚拟机和库兼容。C# 设计者虽然是在现有语言的基础上构建的,但并没有感到那么受限制。他们从一开始就一直在规划泛型,并能够在 .NET 虚拟机中设计大量新的支持。
Several of the key tradeoffs in the design of generics can be illustrated by comparing the features of C++, Java, and C#. C++ is by far the most ambitious of the three. Its templates are intended for almost any programming task that requires substantially similar but not identical copies of an abstraction. Java and C# provide generics purely for the sake of polymorphism. Java's design was heavily influenced by the desire for backward compatibility, not only with existing versions of the language, but with existing virtual machines and libraries. The C# designers, though building on an existing language, did not feel as constrained. They had been planning for generics from the outset, and were able to engineer substantial new support into the .NET virtual machine.
更深入地
IN MORE DEPTH
在配套网站上,我们更详细地讨论了 C++、Java 和 C# 泛型,并考虑了它们的不同设计对错误消息质量、生成代码的速度和大小以及符号的表达能力的影响。我们特别注意到,用于使泛型类和方法支持尽可能广泛的泛型参数类的机制非常不同。
On the companion site we discuss C++, Java, and C# generics in more detail, and consider the impact of their differing designs on the quality of error messages, the speed and size of generated code, and the expressive power of the notation. We note in particular the very different mechanisms used to make generic classes and methods support as broad a class of generic arguments as possible.
对于简单的原始数据类型(例如整数、浮点数或字符),相等性测试和赋值是相对简单的操作,具有明显的语义和明显的实现(逐位比较或复制)。对于更复杂或抽象的数据类型,语义和实现都会出现微妙之处。
For simple, primitive data types such as integers, floating-point numbers, or characters, equality testing and assignment are relatively straightforward operations, with obvious semantics and obvious implementations (bit-wise comparison or copy). For more complicated or abstract data types, both semantic and implementation subtleties arise.
例如,考虑比较两个字符串的问题。表达式s = t是否确定s和t
Consider for example the problem of comparing two character strings. Should the expression s = t determine whether s and t
■ are aliases for one another?
■ 是否占用整个长度上逐位相同的存储空间?
■ occupy storage that is bit-wise identical over its full length?
■ 包含相同的字符序列?
■ contain the same sequence of characters?
■ 打印出来会一样吗?
■ would appear the same if printed?
这些测试中的第二个可能太低级,大多数程序都不会感兴趣;它表明比较可能会因为字符串保留空间中当前未使用的部分存在垃圾而失败。其他三个替代方案在某些情况下可能都很有趣,并且可能会产生不同的结果。
The second of these tests is probably too low level to be of interest in most programs; it suggests the possibility that a comparison might fail because of garbage in currently unused portions of the space reserved for a string. The other three alternatives may all be of interest in certain circumstances, and may generate different results.
在许多情况下,相等的定义归结为左值和右值之间的区别:在存在引用的情况下,表达式是否应仅当它们引用同一个对象时才被视为相等,或者它们引用的对象在某种意义上相等时也被视为相等?第一种选择(引用同一个对象)称为浅比较。第二种选择(引用相等的对象)称为深度比较。对于复杂的数据结构(例如列表或图形),深度比较可能需要递归遍历。
In many cases the definition of equality boils down to the distinction between l-values and r-values: in the presence of references, should expressions be considered equal only if they refer to the same object, or also if the objects to which they refer are in some sense equal? The first option (refer to the same object) is known as a shallow comparison. The second (refer to equal objects) is called a deep comparison. For complicated data structures (e.g., lists or graphs) a deep comparison may require recursive traversal.
在命令式编程语言中,赋值操作也可能是深的或浅的。在变量的引用模型下,浅赋值a := b将使a引用b所引用的对象。深赋值将创建 b 所引用对象的副本,并使 a 引用该副本。在变量的值模型下,浅赋值将把b的值复制到a中,但如果该值是一个指针(或包含指针的记录),则不会复制指针所引用的对象。
In imperative programming languages, assignment operations may also be deep or shallow. Under a reference model of variables, a shallow assignment a := b will make a refer to the object to which b refers. A deep assignment will create a copy of the object to which b refers, and make a refer to the copy. Under a value model of variables, a shallow assignment will copy the value of b into a, but if that value is a pointer (or a record containing pointers), then the objects to which the pointer(s) refer will not be copied.
深度赋值相对较少见。它们主要用于分布式计算,特别是用于远程过程调用 (RPC) 系统中的参数传递。这些将在 C-13.5.4 节中讨论。
Deep assignments are relatively rare. They are used primarily in distributed computing, and in particular for parameter passing in remote procedure call (RPC) systems. These will be discussed in Section C-13.5.4.
对于用户定义的抽象,没有任何一种语言指定的相等性测试或赋值机制可能在所有情况下都产生所需的结果。具有复杂数据抽象机制的语言通常允许程序员为每种新数据类型定义比较和赋值运算符 - 或者指定不允许相等性测试和/或赋值。
For user-defined abstractions, no single language-specified mechanism for equality testing or assignment is likely to produce the desired results in all cases. Languages with sophisticated data abstraction mechanisms usually allow the programmer to define the comparison and assignment operators for each new data type—or to specify that equality testing and/or assignment is not allowed.
本章概述了类型的基本概念。在典型的编程语言中,类型有两个主要用途:它们为许多操作提供隐式上下文,使程序员无需显式指定上下文,并且允许编译器捕获各种常见的编程错误。在讨论类型时,我们注意到区分指称、结构和基于抽象的观点有时会有所帮助,这些观点分别从类型的值、子结构和支持的操作的角度来看待类型。
This chapter has surveyed the fundamental concept of types. In the typical programming language, types serve two principal purposes: they provide implicit context for many operations, freeing the programmer from the need to specify that context explicitly, and they allow the compiler to catch a wide variety of common programming errors. When discussing types, we noted that it is sometimes helpful to distinguish among denotational, structural, and abstraction-based points of view, which regard types, respectively, in terms of their values, their substructure, and the operations they support.
在典型的编程语言中,类型系统由一组内置类型、定义新类型的机制以及类型等价、类型兼容性和类型推断的规则组成。类型等价确定两个值或命名对象何时具有相同类型。类型兼容性确定何时可以在“期望”另一种类型的上下文中使用一种类型的值。类型推断根据表达式组件的类型或(有时)周围上下文确定表达式的类型。如果一种语言从不允许将操作应用于不支持该操作的对象,则称该语言为强类型;如果一种语言在编译时强制执行强类型,则称该语言为静态类型。
In a typical programming language, the type system consists of a set of built-in types, a mechanism to define new types, and rules for type equivalence, type compatibility, and type inference. Type equivalence determines when two values or named objects have the same type. Type compatibility determines when a value of one type maybe used in a context that “expects” another type. Type inference determines the type of an expression based on the types of its components or (sometimes) the surrounding context. A language is said to be strongly typed if it never allows an operation to be applied to an object that does not support it; a language is said to be statically typed if it enforces strong typing at compile time.
我们介绍了常用内置类型、枚举、子范围和常用类型构造函数的术语(后者将在第 8 章中详细介绍)。我们讨论了类型等价、兼容性和推断的几种不同方法。我们还研究了类型转换、强制和非转换强制类型转换。在类型等价方面,我们对比了结构和基于名称的方法,并注意到虽然名称等价似乎越来越受欢迎,但结构等价仍然受到拥护。
We introduced terminology for the common built-in types and for enumerations, subranges, and the common type constructors (more on the latter will appear in Chapter 8). We discussed several different approaches to type equivalence, compatibility, and inference. We also examined type conversion, coercion, and nonconverting casts. In the area of type equivalence, we contrasted the structural and name-based approaches, noting that while name equivalence appears to have gained in popularity, structural equivalence retains its advocates.
我们扩展了3.5.2 节中介绍的内容,探索了几种多态性样式,所有这些样式都允许子程序(或类的方法)操作多种类型的值,只要它们仅以它们的类型支持的方式使用这些值。我们特别关注了参数多态性,其中代码将操作的值的类型作为额外参数传递给它,无论是隐式还是显式。隐式替代方案出现在 ML 及其后代的静态类型中,以及 Lisp、Smalltalk 和许多其他语言的动态类型中。显式替代方案出现在许多现代语言的泛型中。在第 10 章中,我们将考虑与子类型多态性相关的主题。
Expanding on material introduced in Section 3.5.2, we explored several styles of polymorphism, all of which allow a subroutine—or the methods of a class—to operate on values of multiple types, so long as they only use those values in ways their types support. We focused in particular on parametric polymorphism, in which the types of the values on which the code will operate are passed to it as extra parameters, implicitly or explicitly. The implicit alternative appears in the static typing of ML and its descendants, and in the dynamic typing of Lisp, Smalltalk, and many other languages. The explicit alternative appears in the generics of many modern languages. In Chapter 10 we will consider the related topic of subtype polymorphism.
在讨论隐式参数多态性时,我们投入了大量精力在 ML 中的类型检查上,其中编译器使用复杂的推理系统在编译时确定类型错误(尝试对不支持的类型执行操作)是否可能发生在运行时 — 所有这些都无需访问源代码中的类型声明。在讨论泛型时,我们探索了表达泛型参数约束的其他方法。我们还考虑了实现策略。作为示例,我们在配套网站上对比了 C++、Java 和 C# 的泛型功能。
In our discussion of implicit parametric polymorphism, we devoted considerable attention to type checking in ML, where the compiler uses a sophisticated system of inference to determine, at compile time, whether a type error (an attempt to perform an operation on a type that doesn't support it) could ever occur at run time—all without access to type declarations in the source code. In our discussion of generics we explored alternative ways to express constraints on generic parameters. We also considered implementation strategies. As examples, we contrasted (on the companion site) the generic facilities of C++, Java, and C#.
与前面的章节相比,我们对类型的研究或许更能凸显出不同语言设计者在理念上的根本差异。正如我们所见,一些语言使用变量来命名值,而另一些语言则使用引用。一些语言在编译时进行全部或大部分类型检查,而另一些语言则等到运行时才进行。在那些在编译时进行检查的语言中,一些使用名称等价,而另一些使用结构等价。一些语言避免类型强制,而另一些语言则接受它们。一些语言避免重载,而另一些语言又接受它们。在每种情况下,设计方案的选择都反映了相互竞争的语言目标之间非平凡的权衡,包括表现力、编程的简易性、错误发现的质量和时间、调试和维护的简易性、编译成本以及运行时性能。
More so, perhaps, than in previous chapters, our study of types has highlighted fundamental differences in philosophy among language designers. As we have seen, some languages use variables to name values; others, references. Some languages do all or most of their type checking at compile time; others wait until run time. Among those that check at compile time, some use name equivalence; others, structural equivalence. Some languages avoid type coercions; others embrace them. Some avoid overloading; others again embrace them. In each case, the choice among design alternatives reflects nontrivial tradeoffs among competing language goals, including expressiveness, ease of programming, quality and timing of error discovery, ease of debugging and maintenance, compilation cost, and run-time performance.
7.1 自 20 世纪 70 年代以来开发的大多数静态类型语言(包括 Java、C# 和 Pascal 的后代)都使用某种形式的类型名称等价性。结构等价性是不是一个坏主意?为什么?
7.1 Most statically typed languages developed since the 1970s (including Java, C#, and the descendants of Pascal) use some form of name equivalence for types. Is structural equivalence a bad idea? Why or why not?
7.2 在以下代码中,编译器会认为哪些变量在结构等价下具有兼容类型?在严格名称等价下?在宽松名称等价下?类型 T = 整数数组 [1..10] S =T A : T B : T C : S D : 整数数组 [1..10]
7.2 In the following code, which of the variables will a compiler consider to have compatible types under structural equivalence? Under strict name equivalence? Under loose name equivalence?
type T = array [1..10] of integer
S =T
A : T
B : T
C : S
D : array [1..10] of integer
7.3 考虑以下声明:1. type cell --前向声明2. type cell_ptr = 指向 cell 的指针3. x : cell 4. type cell = record 5. val : integer 6. next : cell_ptr 7. y : cell第 4 行的声明是否应该被说成是引入了别名类型?在严格名称等价性下,x和y是否应该具有相同的类型?解释一下。
7.3 Consider the following declarations:
1. type cell --a forward declaration
2. type cell_ptr = pointer to cell
3. x : cell
4. type cell = record
5. val : integer
6. next : cell_ptr
7. y : cell
Should the declaration at line 4 be said to introduce an alias type? Under strict name equivalence, should x and y have the same type? Explain.
7.4 假设您正在实现一个 Ada 编译器,并且必须支持对具有程序员指定小数位数的 32 位定点二进制数进行算术运算。描述您需要生成的对两个定点数进行加、减、乘或除的代码。您应该假设硬件只提供整数和 IEEE 浮点的算术指令。您可以假设整数指令保留全精度;特别是,整数乘法产生 64 位结果。您的描述应该足够通用,以处理具有不同小数位数的操作数和结果。
7.4 Suppose you are implementing an Ada compiler, and must support arithmetic on 32-bit fixed-point binary numbers with a programmer-specified number of fractional bits. Describe the code you would need to generate to add, subtract, multiply, or divide two fixed-point numbers. You should assume that the hardware provides arithmetic instructions only for integers and IEEE floating-point. You may assume that the integer instructions preserve full precision; in particular, integer multiplication produces a 64-bit result. Your description should be general enough to deal with operands and results that have different numbers of fractional bits.
7.5 20 世纪 80 年代初,当 Sun Microsystems 将 Berkeley Unix 从 Digital VAX 移植到 Motorola 680x0 时,许多 C 语言程序停止运行,必须进行修复。实际上,680x0 暴露了某些类型的程序错误,这些错误在 VAX 上是可以“侥幸逃脱”的。其中一类错误出现在使用多种整数大小(例如短整数和长整数)的程序中,这是因为 VAX 是小端机,而 680x0 是大端机(第 C-5.2 节)。另一类错误出现在同时操作空字符串和空字符串的程序中。它出现在因为 VAX 上 Unix 进程地址空间中的零位置始终包含零,而 680x0 上的相同位置不在该地址空间中,如果使用则会产生保护错误。对于这两类错误,请给出可以在 VAX 上运行但在 680x0 上无法运行的程序片段的示例。
7.5 When Sun Microsystems ported Berkeley Unix from the Digital VAX to the Motorola 680x0 in the early 1980s, many C programs stopped working, and had to be repaired. In effect, the 680x0 revealed certain classes of program bugs that one could “get away with” on the VAX. One of these classes of bugs occurred in programs that use more than one size of integer (e.g., short and long), and arose from the fact that the VAX is a little-endian machine, while the 680x0 is big-endian (Section C-5.2). Another class of bugs occurred in programs that manipulate both null and empty strings. It arose from the fact that location zero in a Unix process's address space on the VAX always contained a zero, while the same location on the 680x0 is not in the address space, and will generate a protection error if used. For both of these classes of bugs, give examples of program fragments that would work on a VAX but not on a 680x0.
7.6 Ada 为整数类型提供了两个“余数”运算符,rem和mod,定义如下 [ Ame83,第 4.5.5 节]:
7.6 Ada provides two “remainder” operators, rem and mod for integer types, defined as follows [Ame83, Sec. 4.5.5]:
整数除法和余数由关系A = (A/B)*B + (A rem B)定义,其中(A rem B)具有 A 的符号且绝对值小于 B 的绝对值。整数除法满足恒等式(-A)/B = -(A/B) = A/(-B)。
Integer division and remainder are defined by the relation A = (A/B)*B + (A rem B), where (A rem B) has the sign of A and an absolute value less than the absolute value of B. Integer division satisfies the identity (-A)/B = -(A/B) = A/(-B).
模数运算的结果是(A mod B)具有B的符号和小于B的绝对值的绝对值;此外,对于某个整数值N,该结果必须满足关系A = B*N + (A mod B)。
The result of the modulus operation is such that (A mod B) has the sign of B and an absolute value less than the absolute value of B; in addition, for some integer value N, this result must satisfy the relation A = B*N + (A mod B).
给出A和B的值,其中A rem B和A mod B不同。出于什么目的,一个操作会比另一个更有用?同时提供两个操作是否有意义,还是有点过头了?
再考虑一下 C 的 % 运算符和 Pascal 的 mod 运算符。这些语言的设计者可以选择类似于 Ada 的rem或其mod的语义。他们选择了哪一个?你认为他们的选择正确吗?
Give values of A and B for which A rem B and A mod B differ. For what purposes would one operation be more useful than the other? Does it make sense to provide both, or is it overkill?
Consider also the % operator of C and the mod operator of Pascal. The designers of these languages could have picked semantics resembling those of either Ada's rem or its mod. Which did they pick? Do you think they made the right choice?
7.7 考虑在 Pascal 中对集合表达式执行范围检查的问题。假设一个集合可能包含许多元素,其中一些元素在编译时可能是已知的,请描述编译器可能维护的信息,以便跟踪已知属于该集合的元素和未知元素的可能范围。然后解释如何为以下集合运算更新此信息:并集、交集和差集。目标是确定 (1) 何时可以在运行时消除子范围检查以及 (2) 何时可以在编译时报告子范围错误。请记住,编译器不可能做得完美:不可避免地会执行一些不必要的运行时检查,并且一些必然会导致错误的操作将不会在编译时被捕获。目标是以合理的成本尽可能好地完成工作。
7.7 Consider the problem of performing range checks on set expressions in Pascal. Given that a set may contain many elements, some of which may be known at compile time, describe the information that a compiler might maintain in order to track both the elements known to belong to the set and the possible range of unknown elements. Then explain how to update this information for the following set operations: union, intersection, and difference. The goal is to determine (1) when subrange checks can be eliminated at run time and (2) when subrange errors can be reported at compile time. Bear in mind that the compiler cannot do a perfect job: some unnecessary run-time checks will inevitably be performed, and some operations that must always result in errors will not be caught at compile time. The goal is to do as good a job as possible at reasonable cost.
7.8 在7.2.2 节中,我们引入了通用引用类型(C 语言中的void *)的概念,该类型引用未知类型的对象。使用此类引用,按照7.3.1 节中的建议,在 C 语言中实现“穷人的通用队列” 。哪些地方需要类型转换?为什么?给出一个由于缺乏类型检查而在运行时导致灾难性失败的队列使用示例。
7.8 In Section 7.2.2 we introduced the notion of a universal reference type (void * in C) that refers to an object of unknown type. Using such references, implement a “poor man's generic queue” in C, as suggested in Section 7.3.1. Where do you need type casts? Why? Give an example of a use of the queue that will fail catastrophically at run time, due to the lack of type checking.
7.9 用 Ada、Java 或 C#重写图 7.3的代码。
7.9 Rewrite the code of Figure 7.3 in Ada, Java, or C#.
7.10
7.10
(a)给出 练习 6.19 的通用解决方案。
(a) Give a generic solution to Exercise 6.19.
(b) 将该解决方案翻译成 Ada、Java 或 C#。
(b) Translate this solution into Ada, Java, or C#.
7.11 使用你最喜欢的具有泛型的语言,编写以下抽象的简单版本的代码:
7.11 In your favorite language with generics, write code for simple versions of the following abstractions:
(a) a stack, implemented as a linked list
(b) 优先级队列,以跳跃表或嵌入数组的偏序树的形式实现。
(b) a priority queue, implemented as a skip list or a partially ordered tree embedded in an array
(c) 以哈希表形式实现的字典(映射)
(c) a dictionary (mapping), implemented as a hash table
7.12 图 7.3将整数max_items作为通用参数传递给队列抽象。编写代码的替代版本,将max_items作为队列构造函数的参数。通用参数版本的优点是什么?
7.12 Figure 7.3 passes integer max_items to the queue abstraction as a generic parameter. Write an alternative version of the code that makes max_items a parameter to the queue constructor instead. What is the advantage of the generic parameter version?
7.13使用 OCaml 或 SML 函子重写 示例 7.50 – 7.52的通用排序例程(带有约束)。
7.13 Rewrite the generic sorting routine of Examples 7.50–7.52 (with constraints) using OCaml or SML functors.
7.14 充实示例 7.53中的 C++ 排序例程。说明当要求对char*字符串数组进行排序时,此例程会“做错事” 。
7.14 Flesh out the C++ sorting routine of Example 7.53. Demonstrate that this routine does “the wrong thing” when asked to sort an array of char* strings.
7.15 在示例 7.53中,我们提到了在 C++ 中定义通用排序例程时使比较需求更加明确的三种方法:将比较例程设为通用参数类T的方法、排序例程的额外参数或额外的通用参数。实现这些选项并讨论它们的优缺点。
7.15 In Example 7.53 we mentioned three ways to make the need for comparisons more explicit when defining a generic sort routine in C++: make the comparison routine a method of the generic parameter class T, an extra argument to the sort routine, or an extra generic parameter. Implement these options and discuss their comparative strengths and weaknesses.
7.16 上一个练习中的问题的另一个解决方案是将排序例程设为排序器类的一个方法。然后可以将比较例程作为构造函数参数传递到该类中。实现此选项并将其与上一个练习中的选项进行比较。
7.16 Yet another solution to the problem of the previous exercise is to make the sorting routine a method of a sorter class. The comparison routine can then be passed into the class as a constructor argument. Implement this option and compare it to those of the previous exercise.
7.17 考虑以下 C++ 代码框架:#include <list> using std::list; class foo { … class bar : public foo { … static void print_all(list<foo*> &L) { … list<foo*> LF; list<bar*> LB; … print_all(LF); // 工作正常print_all(LB); // 静态语义错误解释为什么编译器不允许第二次调用。举一个例子说明如果允许,可能发生的坏事。
7.17 Consider the following code skeleton in C++:
#include <list>
using std::list;
class foo { …
class bar : public foo { …
static void print_all(list<foo*> &L) { …
list<foo*> LF;
list<bar*> LB;
…
print_all(LF); // works fine
print_all(LB); // static semantic error
Explain why the compiler won't allow the second call. Give an example of bad things that could happen if it did.
7.18 C++ 的最初设计者 Bjarne Stroustrup 曾将模板描述为“一种遵循 C++ 的作用域、命名和类型规则的巧妙的宏” [ Str13,第二版,第 257 页]。两者的相似性有多高?模板能做什么而宏不能做什么?宏能做什么而模板不能做什么?
7.18 Bjarne Stroustrup, the original designer of C++, once described templates as “a clever kind of macro that obeys the scope, naming, and type rules of C++” [Str13, 2nd ed., p. 257]. How close is the similarity? What can templates do that macros can't? What do macros do that templates don't?
7.19 在9.3.1 节中,我们注意到 Ada 83 不允许将子程序作为参数传递,但是使用泛型可以实现一些相同的效果。假设我们想要将一个函数应用于数组的每个成员。我们可以在 Ada 83 中编写以下内容:泛型类型 item 是 private;类型 item_array 是 item 的数组 (integer range <>);使用函数 F(it : in item) return item; procedure apply_to_array(A : in out item_array); procedure apply_to_array(A : in out item_array) is begin for i in A'first..A'last loop A(i) := F(A(i)); end loop; end apply_to_array;给定一个整数数组scores和一个整数函数foo,我们可以编写:procedure apply_to_ints is new apply_to_array(integer, int_array, foo); … apply_to_ints(scores);这种机制有多通用?它的局限性是什么?它是否是正式(即第二类,而不是第三类)子程序的合理替代品?
7.19 In Section 9.3.1 we noted that Ada 83 does not permit subroutines to be passed as parameters, but that some of the same effect can be achieved with generics. Suppose we want to apply a function to every member of an array. We might write the following in Ada 83:
generic
type item is private;
type item_array is array (integer range <>) of item;
with function F(it : in item) return item;
procedure apply_to_array(A : in out item_array);
procedure apply_to_array(A : in out item_array) is
begin
for i in A'first..A'last loop
A(i) := F(A(i));
end loop;
end apply_to_array;
Given an array of integers, scores, and a function on integers, foo, we can write:
procedure apply_to_ints is
new apply_to_array(integer, int_array, foo);
…
apply_to_ints(scores);
How general is this mechanism? What are its limitations? Is it a reasonable substitute for formal (i.e., second-class, as opposed to third-class) subroutines?
7.20修改 图 7.3的代码或练习 7.12的解决方案,使其在尝试将项目放入已满队列中或从空队列中出队时引发异常。
7.20 Modify the code of Figure 7.3 or your solution to Exercise 7.12 to throw an exception if an attempt is made to enqueue an item in a full queue, or to dequeue an item from an empty queue.
7.21–7.27 更深入。
7.21–7.27 In More Depth.
7.28 一些语言定义指定了数据类型在内存中的特定表示,而另一些语言定义仅指定了这些类型的语义行为。对于后一类语言,一些实现保证了特定的表示,而另一些实现保留了在不同情况下选择不同表示的权利。您更喜欢哪种方法?为什么?
7.28 Some language definitions specify a particular representation for data types in memory, while others specify only the semantic behavior of those types. For languages in the latter class, some implementations guarantee a particular representation, while others reserve the right to choose different representations in different circumstances. Which approach do you prefer? Why?
7.29 研究Strom 等人在 Hermes 编程语言中采用的类型状态机制 [ SBG + 91 ]。讨论其与 Java 和 C# 中的明确赋值概念的关系(第 6.1.3 节)。
7.29 Investigate the typestate mechanism employed by Strom et al. in the Hermes programming language [SBG+91]. Discuss its relationship to the notion of definite assignment in Java and C# (Section 6.1.3).
7.30 最近有几个项目试图通过在脚本语言中添加可选的类型声明来模糊静态和动态类型之间的界限。这些声明支持渐进式类型化策略,即程序员最初以传统的脚本风格编写,然后逐步添加声明以提高可靠性或降低运行时成本。了解分别由 Google、Facebook 和 Microsoft 推广的 Dart、Hack 和 TypeScript 语言。你对此有何印象?你认为在实践中将声明改造成最初没有声明的程序有多容易?
7.30 Several recent projects attempt to blur the line between static and dynamic typing by adding optional type declarations to scripting languages. These declarations support a strategy of gradual typing, in which programmers initially write in a traditional scripting style and then add declarations incrementally to increase reliability or decrease run-time cost. Learn about the Dart, Hack, and TypeScript languages, promoted by Google, Facebook, and Microsoft, respectively. What are your impressions? How easy do you think it will be in practice to retrofit declarations into programs originally developed without them?
7.31 研究标准 ML、OCaml、Haskell 和 F# 的类型系统。它们的主要区别是什么?语言设计者做出的不同选择可能由什么原因造成?
7.31 Research the type systems of Standard ML, OCaml, Haskell, and F#. What are the principal differences? What might explain the different choices made by the language designers?
7.32 用 C++ 或 Ada 编写一个程序,从同一模板/通用类型创建至少两个具体类型或子例程。将代码编译为汇编语言并查看结果。描述从源代码到目标代码的映射。
7.32 Write a program in C++ or Ada that creates at least two concrete types or subroutines from the same template/generic. Compile your code to assembly language and look at the result. Describe the mapping from source to target code.
7.33 虽然 Haskell 不包含泛型(其参数多态性是隐式的),但其类型类可以被视为类型约束的泛化。了解有关类型类的更多信息。讨论它们与多态函数的相关性以及更一般的用途。您可能希望提前阅读第 11.5.2 节中有关monad的讨论。
7.33 While Haskell does not include generics (its parametric polymorphism is implicit), its type classes can be considered a generalization of type constraints. Learn more about type classes. Discuss their relevance to polymorphic functions, as well as more general uses. You might want to look ahead to the discussion of monads in Section 11.5.2.
7.34研究 Black 等人在 Emerald 编程语言 [ BHJL07 ] 中采用的类型一致性概念。讨论一致性与 ML 的类型推断以及面向对象语言的基于类的类型化之间的关系。
7.34 Investigate the notion of type conformance, employed by Black et al. in the Emerald programming language [BHJL07]. Discuss how conformance relates to the type inference of ML and to the class-based typing of object-oriented languages.
7.35 C++11 引入了所谓的可变参数模板,它采用可变数量的泛型参数。阅读它们的工作原理。说明如何使用它们将格式化输出的常用语法cout << expr 1 << ... << expr n替换为print ( expr 1 , ..., expr n ),同时保留完整的静态类型检查。
7.35 C++11 introduces so-called variadic templates, which take a variable number of generic parameters. Read up on how these work. Show how they might be used to replace the usual cout << expr1 << … << exprn syntax of formatted output with print(expr1, …, exprn), while retaining full static type checking.
7.36–7.38 更深入。
7.36–7.38 In More Depth.
有关本章中提到的各种编程语言的一般信息,请参见附录 A以及第 1 章和第6 章的参考书目注释。Welsh、Sneeringer 和 Hoare [ WSH77 ] 对原始 Pascal 定义进行了批评,特别强调了其类型系统。Tanenbaum 对 Pascal 和 Algol 68 的比较也主要侧重于类型 [ Tan78 ]。Cleaveland [ Cle86 ] 对本章中的许多问题进行了一本书长度的研究。Pierce [ Pie02 ] 对这个主题进行了正式而详细的现代报道。ACM 编程语言特别兴趣小组于 2003 年启动了每两年一次的语言设计和实现类型研讨会。
References to general information on the various programming languages mentioned in this chapter can be found in Appendix A, and in the Bibliographic Notes for Chapters 1 and 6. Welsh, Sneeringer, and Hoare [WSH77] provide a critique of the original Pascal definition, with a particular emphasis on its type system. Tanenbaum's comparison of Pascal and Algol 68 also focuses largely on types [Tan78]. Cleaveland [Cle86] provides a book-length study of many of the issues in this chapter. Pierce [Pie02] provides a formal and detailed modern coverage of the subject. The ACM Special Interest Group on Programming Languages launched a biennial workshop on Types in Language Design and Implementation in 2003.
我们所说的类型的外延模型源自 Hoare [ DDH72 ]。第 4 章的参考书目注释中讨论了编程语言整体语义的外延公式。一项相关但不同的工作使用代数技术来形式化数据抽象;主要参考文献包括 Guttag [ Gut77 ] 和 Goguen 等人 [ GTW78 ]。Milner 的原始论文 [ Mil78 ] 是关于 ML 中类型推断的开创性参考文献。Mairson [ Mai90 ] 证明统一 ML 类型的成本为O (2n ),其中n是程序的长度。幸运的是,成本与程序的类型表达式的大小成线性关系,因此最坏的情况只会出现在语义过于复杂以至于人类无法理解的程序中。
What we have referred to as the denotational model of types originates with Hoare [DDH72]. Denotational formulations of the overall semantics of programming languages are discussed in the Bibliographic Notes for Chapter 4. A related but distinct body of work uses algebraic techniques to formalize data abstraction; key references include Guttag [Gut77] and Goguen et al. [GTW78]. Milner's original paper [Mil78] is the seminal reference on type inference in ML. Mairson [Mai90] proves that the cost of unifying ML types is O(2n),where n is the length of the program. Fortunately, the cost is linear in the size of the program's type expressions, so the worst case arises only in programs whose semantics are too complex for a human being to understand anyway.
Hoare [ Hoa75 ] 讨论了变量引用模型下递归类型的定义。Cardelli 和 Wegner 调查了与多态性、重载和抽象相关的问题 [ CW85 ]。万维网的字符模型标准对多语言字符集的微妙之处和复杂性进行了极具可读性的介绍 [ Wor05 ]。
Hoare [Hoa75] discusses the definition of recursive types under a reference model of variables. Cardelli and Wegner survey issues related to polymorphism, overloading, and abstraction [CW85]. The Character Model standard for the World Wide Web provides a remarkably readable introduction to the subtleties and complexities of multilingual character sets [Wor05].
Garcia 等人对 ML、C++、Haskell、Eiffel、Java 和 C# [ GJL + 03 ] 中的泛型功能进行了详细比较。Kennedy 和 Syme [ KS01 ]描述了 C# 的泛型功能。Java 泛型基于 Bracha 等人的工作 [ BOSW98 ]。Erwin Unruh 因发现 C++ 模板可以诱使编译器执行非平凡计算而受到赞誉。他的具体示例 ( www.erwin-unruh.de/primorig.html ) 并未编译,但会导致编译器生成一系列包含前n 个素数的错误消息。Abrahams 和 Gurtovoy 提供了一本书长度的模板元编程[ AG05 ] 的论述,该领域源于这一发现。
Garcia et al. provide a detailed comparison of generic facilities in ML, C++, Haskell, Eiffel, Java, and C# [GJL+03]. The C# generic facility is described by Kennedy and Syme [KS01]. Java generics are based on the work of Bracha et al. [BOSW98]. Erwin Unruh is credited with discovering that C++ templates could trick the compiler into performing nontrivial computation. His specific example (www.erwin-unruh.de/primorig.html) did not compile, but caused the compiler to generate a sequence of n error messages, embedding the first n primes. Abrahams and Gurtovoy provide a book-length treatment of template metaprogramming [AG05], the field that grew out of this discovery.
第 7 章 介绍了类型的概念作为一种组织计算机程序操纵的许多值和对象的方式。它还引入了内置类型和复合类型的术语。正如我们在7.1.4 节中提到的,复合类型是通过使用类型构造函数将一个或多个更简单的类型连接在一起而形成的。从指称的角度来看,构造函数可以建模为集合上的操作,每个集合代表一个更简单的类型。
Chapter 7 introduced the notion of types as a way to organize the many values and objects manipulated by computer programs. It also introduced terminology for both built-in and composite types. As we noted in Section 7.1.4, composite types are formed by joining together one or more simpler types using a type constructor. From a denotational perspective, the constructors can be modeled as operations on sets, with each set representing one of the simpler types.
在本章中,我们将概述最重要的类型构造函数:记录、数组、字符串、集合、指针、列表和文件。在记录部分,我们还将考虑变体(联合)和元组。在指针部分,我们将更详细地介绍第6.1.2 节中介绍的变量的值和引用模型,以及第 3.2 节中介绍的堆管理问题。文件部分(主要在配套网站上)将讨论输入和输出机制。
In the current chapter we will survey the most important type constructors: records, arrays, strings, sets, pointers, lists, and files. In the section on records we will also consider both variants (unions) and tuples. In the section on pointers, we will take a more detailed look at the value and reference models of variables introduced in Section 6.1.2, and the heap management issues introduced in Section 3.2. The section on files (mostly on the companion site) will include a discussion of input and output mechanisms.
记录类型允许将异构类型的相关数据一起存储和操作。记录最初由 Cobol 引入,也出现在 Algol 68 中,后者将其称为结构体,并引入了关键字struct。许多现代语言,包括 C 及其后代,都使用了 Algol 术语。Fortran 90 简单地将其记录称为“类型”:它们是除数组之外唯一一种由程序员定义的类型,数组有自己特殊的语法。C++ 中的结构体被定义为类的一种特殊形式(默认情况下,其成员是全局可见的)。Java 没有struct的特殊概念;它的程序员在所有情况下都使用类。C# 和 Swift 对类类型的变量使用引用模型,对结构类型的变量使用值模型。在这些语言中,结构体不支持继承。为简单起见,在大部分讨论中,我们将使用术语“记录”来指代所有这些语言中的相关结构体。
Record types allow related data of heterogeneous types to be stored and manipulated together. Originally introduced by Cobol, records also appeared in Algol 68, which called them structures, and introduced the keyword struct. Many modern languages, including C and its descendants, employ the Algol terminology. Fortran 90 simply calls its records “types”: they are the only form of programmer-defined type other than arrays, which have their own special syntax. Structures in C++ are defined as a special form of class (one in which members are globally visible by default). Java has no distinguished notion of struct; its programmers use classes in all cases. C# and Swift use a reference model for variables of class types, and a value model for variables of struct types. In these languages, structs do not support inheritance. For the sake of simplicity, we will use the term “record” in most of our discussion to refer to the relevant construct in all these languages.
记录的字段通常存储在内存中的相邻位置。在其符号表中,编译器会跟踪每个记录类型中每个字段的偏移量。当需要访问字段时,编译器通常会生成带有位移寻址的加载或存储指令。对于本地对象,基址寄存器通常是帧指针;位移是记录与寄存器的偏移量与字段在记录中的偏移量之和。
The fields of a record are usually stored in adjacent locations in memory. In its symbol table, the compiler keeps track of the offset of each field within each record type. When it needs to access a field, the compiler will often generate a load or store instruction with displacement addressing. For a local object, the base register is typically the frame pointer; the displacement is then the sum of the record's offset from the register and the field's offset within the record.
对于小记录,复制和比较都可以逐个字段地在线执行。对于较长的记录,我们可以通过推迟到库例程来显著节省代码空间。block_copy 例程可以将源地址、目标地址和长度作为参数,但类似的block_compare例程将无法处理孔中含有不同(垃圾)数据的记录。一种解决方案是安排所有孔都包含一些可预测的值(例如零),但这需要在每个细化点都编写代码。另一种方法是让编译器为每种记录类型生成自定义的逐个字段比较例程。将调用不同的例程来比较不同类型的记录。
For small records, both copies and comparisons can be performed in-line on a field-by-field basis. For longer records, we can save significantly on code space by deferring to a library routine. A block_copy routine can take source address, destination address, and length as arguments, but the analogous block_compare routine would fail on records with different (garbage) data in the holes. One solution is to arrange for all holes to contain some predictable value (e.g., zero), but this requires code at every elaboration point. Another is to have the compiler generate a customized field-by-field comparison routine for every record type. Different routines would be called to compare records of different types.
在大多数情况下,字段的重新排序纯粹是一个实现问题:只要记录类型的所有实例都以相同的方式重新排序,程序员就不需要知道这一点。例外发生在系统程序中,系统程序有时会“查看”数据类型的实现,期望它将以特定的方式映射到内存中。例如,内核程序员可能依靠特定的布局策略来定义记录模仿特定以太网设备的内存映射控制寄存器的组织。C 和 C++ 主要是为系统程序设计的,它们保证结构体的字段将按照声明的顺序分配。保证第一个字段具有硬件对任何类型的要求的最粗对齐(通常是四字节或八字节边界)。后续字段具有其类型的自然对齐。Fortran 90 允许程序员指定字段不得重新排序;如果没有这样的规范,编译器可以选择自己的顺序。为了适应系统程序,Ada、C 和 C++ 都允许程序员精确指定记录的每个字段应占用多少位。如果“packed”指令本质上是程序员优先级的非约束性指示,则字段声明的位长度是汇编级布局的约束性规范。
In most cases, reordering of fields is purely an implementation issue: the programmer need not be aware of it, so long as all instances of a record type are reordered in the same way. The exception occurs in systems programs, which sometimes “look inside” the implementation of a data type with the expectation that it will be mapped to memory in a particular way. A kernel programmer, for example, may count on a particular layout strategy in order to define a record that mimics the organization of memory-mapped control registers for a particular Ethernet device. C and C++, which are designed in large part for systems programs, guarantee that the fields of a struct will be allocated in the order declared. The first field is guaranteed to have the coarsest alignment required by the hardware for any type (generally a four- or eight-byte boundary). Subsequent fields have the natural alignment for their type. Fortran 90 allows the programmer to specify that fields must not be reordered; in the absence of such a specification the compiler can choose its own order. To accommodate systems programs, Ada, C, and C++ all allow the programmer to specify exactly how many bits to devote to each field of a record. Where a “packed” directive is essentially a nonbinding indication of the programmer's priorities, bit lengths on field declarations are a binding specification of assembly-level layout.
在实践中,联合主要有两个用途。第一个用途出现在系统程序中,联合允许在同一组字节中解释在不同时间以不同的方式实现。典型示例出现在内存管理中,其中存储有时可能被视为未分配空间(可能需要“清零”),有时被视为簿记信息(长度和标头字段,用于跟踪空闲和已分配的块),有时被视为用户分配的任意类型的数据。虽然非转换类型转换(第7.2.1 节)可用于实现堆管理例程,但联合更能表明程序员的意图:这些位不会被重新解释,而是用于独立目的。1
In practice, unions have been used for two main purposes. The first arises in systems programs, where unions allow the same set of bytes to be interpreted in different ways at different times. The canonical example occurs in memory management, where storage may sometimes be treated as unallocated space (perhaps in need of “zeroing out”), sometimes as bookkeeping information (length and header fields to keep track of free and allocated blocks), and sometimes as user-allocated data of arbitrary type. While nonconverting type casts (Section 7.2.1) can be used to implement heap management routines, unions are a better indication of the programmer's intent: the bits are not being reinterpreted, they are being used for independent purposes.1
更深入地
IN MORE DEPTH
我们在配套网站上更详细地讨论了联合和变体记录。我们考虑的主题包括语法、安全性和内存布局问题。安全性是一个特别令人担忧的问题:非转换类型转换允许程序员明确地规避语言的类型系统,而对联合的简单实现很容易意外地做到这一点。Ada 对联合和变体记录的使用施加了限制,允许编译器静态地验证所有程序是否都是类型安全的。我们还注意到,在大多数情况下,面向对象语言中的继承为类型安全的变体记录提供了一种有吸引力的替代方案。这一观察结果在很大程度上解释了大多数较新的语言中省略联合和变体记录的原因。
We discuss unions and variant records in more detail on the companion site. Topics we consider include syntax, safety, and memory layout issues. Safety is a particular concern: where nonconverting type casts allow a programmer to circumvent the language's type system explicitly, a naive realization of unions makes it easy to do so by accident. Ada imposes limits on the use of unions and variant records that allow the compiler to verify, statically, that all programs are type-safe. We also note that inheritance in object-oriented languages provides an attractive alternative to type-safe variant records in most cases. This observation largely accounts for the omission of unions and variant records from most more recent languages.
数组是最常见和最重要的复合数据类型。从 Fortran I 开始,它们已成为几乎所有高级语言的基本组成部分。与将不同类型的相关字段分组的记录不同,数组通常是同质的。从语义上讲,它们可以被认为是从索引类型到组件或元素类型的映射。某些语言(例如 Fortran)要求索引类型为整数;许多语言允许它是任何离散类型。某些语言(例如 Fortran 77)要求数组的元素类型为标量。大多数(包括 Fortran 90)允许任何元素类型。
Arrays are the most common and important composite data types. They have been a fundamental part of almost every high-level language, beginning with Fortran I. Unlike records, which group related fields of disparate types, arrays are usually homogeneous. Semantically, they can be thought of as a mapping from an index type to a component or element type. Some languages (e.g., Fortran) require that the index type be integer; many languages allow it to be any discrete type. Some languages (e.g., Fortran 77) require that the element type of an array be scalar. Most (including Fortran 90) allow any element type.
一些语言(尤其是脚本语言,但也包括一些较新的命令式语言,包括 Go 和 Swift)允许非离散索引类型。 生成的关联数组通常必须用哈希表或搜索树实现;我们将在14.4.3 节中考虑它们。 关联数组也类似于许多面向对象语言的标准库支持的字典或映射类型。 在 C++ 中,运算符重载允许这些类型使用传统的类似数组的语法。 出于本章的目的,我们假设数组索引是离散的。 这允许一种(更高效的)连续分配方案,将在第 8.2.3 节中描述。 我们还假设数组是密集的——它们的大部分元素不等于零或其他默认值。 替代方案——稀疏数组——出现在许多重要的科学问题中。 对于这些问题,库(或在极少数情况下,语言本身)可能支持明确枚举非默认值的替代实现。
Some languages (notably scripting languages, but also some newer imperative languages, including Go and Swift) allow nondiscrete index types. The resulting associative arrays must generally be implemented with hash tables or search trees; we consider them in Section 14.4.3. Associative arrays also resemble the dictionary or map types supported by the standard libraries of many object-oriented languages. In C++, operator overloading allows these types to use conventional array-like syntax. For the purposes of this chapter, we will assume that array indices are discrete. This admits a (much more efficient) contiguous allocation scheme, to be described in Section 8.2.3. We will also assume that arrays are dense—that a large fraction of their elements are not equal to zero or some other default value. The alternative—sparse arrays—arises in many important scientific problems. For these, libraries (or, in rare cases, the language itself) may support an alternative implementation that explicitly enumerates only the non-default values.
大多数语言通过在数组名称后附加下标(通常用方括号分隔)来引用数组元素:A[3]。少数语言(尤其是 Fortran 和 Ada)改用括号:A(3)。
Most languages refer to an element of an array by appending a subscript—usually delimited by square brackets—to the name of the array: A[3]. A few languages— notably Fortran and Ada—use parentheses instead: A(3).
在大多数语言中,数组上允许的唯一操作是选择元素(然后可以将其用于对其类型有效的任何操作)和赋值。少数语言(例如 Ada 和 Fortran 90)允许比较数组是否相等。Ada 允许对元素离散的一维数组进行字典顺序比较:如果A中第一个不等于B中相应元素的元素小于该相应元素,则A < B。Ada还允许将内置逻辑运算符(或、与、异或)应用于布尔数组。
In most languages, the only operations permitted on an array are selection of an element (which can then be used for whatever operations are valid on its type), and assignment. A few languages (e.g., Ada and Fortran 90) allow arrays to be compared for equality. Ada allows one-dimensional arrays whose elements are discrete to be compared for lexicographic ordering: A < B if the first element of A that is not equal to the corresponding element of B is less than that corresponding element. Ada also allows the built-in logical operators (or, and, xor) to be applied to Boolean arrays.
Fortran 90 具有一组非常丰富的数组操作:以整个数组为参数的内置操作。由于 Fortran 使用结构类型等价,因此数组运算符的操作数只需具有相同的元素类型和形状。特别是,相同形状的切片可以在数组操作中混合使用,即使它们被切片的数组具有非常不同的形状。任何内置算术运算符都将数组作为操作数;结果是一个与操作数形状相同的数组,其元素是将运算符应用于相应元素的结果。举一个简单的例子,A + B是一个数组,其每个元素都是A和B的相应元素的总和。 Fortran 90 还提供了大量的内在函数或内置函数。其中 60 多个(包括逻辑和位操作、三角学、对数和指数、类型转换和字符串操作)是在标量上定义的,但如果传递数组作为参数,它们也会逐个元素执行操作。例如,函数tan(A)返回一个由A元素的切线组成的数组。许多其他内在函数仅在数组上定义。这些包括搜索和总结,转置,重塑和下标排列。
Fortran 90 has a very rich set of array operations: built-in operations that take entire arrays as arguments. Because Fortran uses structural type equivalence, the operands of an array operator need only have the same element type and shape. In particular, slices of the same shape can be intermixed in array operations, even if the arrays from which they were sliced have very different shapes. Any of the built-in arithmetic operators will take arrays as operands; the result is an array, of the same shape as the operands, whose elements are the result of applying the operator to corresponding elements. As a simple example, A + B is an array each of whose elements is the sum of the corresponding elements of A and B. Fortran 90 also provides a huge collection of intrinsic, or built-in functions. More than 60 of these (including logic and bit manipulation, trigonometry, logs and exponents, type conversion, and string manipulation) are defined on scalars, but will also perform their operation element-wise if passed arrays as arguments. The function tan(A), for example, returns an array consisting of the tangents of the elements of A. Many additional intrinsic functions are defined solely on arrays. These include searching and summarization, transposition, and reshaping and subscript permutation.
Fortran 90 在很大程度上借鉴了 APL 的灵感,APL 是 Iverson 等人在 20 世纪 60 年代初期至中期开发的一种数组操作语言。2 APL主要设计为数组操作的简洁数学符号。它采用了庞大的字符集,这使其难以与传统键盘和文本显示一起使用。它的变量都是数组,许多特殊字符表示数组操作。APL 实现设计用于解释、交互使用。它们最适合“快速而粗略”地解决数学问题。非常强大的运算符与非常简洁的符号相结合,使得 APL 程序非常难以阅读和理解。J 是 APL 的后继者,使用常规字符集。
Fortran 90 draws significant inspiration from APL, an array manipulation language developed by Iverson and others in the early to mid-1960s.2 APL was designed primarily as a terse mathematical notation for array manipulations. It employs an enormous character set that made it difficult to use with traditional keyboards and textual displays. Its variables are all arrays, and many of the special characters denote array operations. APL implementations are designed for interpreted, interactive use. They are best suited to “quick and dirty” solution of mathematical problems. The combination of very powerful operators with very terse notation makes APL programs notoriously difficult to read and understand. J, a successor to APL, uses a conventional character set.
在上一小节的所有示例中,数组的形状(包括边界)都是在声明中指定的。对于这种静态形状的数组,可以按通常的方式管理存储:对于生存期为整个程序的数组,使用静态分配;对于生存期为子例程调用的数组,使用堆栈分配;对于具有更通用生存期的动态分配数组,使用堆分配。
In all of the examples in the previous subsection, the shape of the array (including bounds) was specified in the declaration. For such static shape arrays, storage can be managed in the usual way: static allocation for arrays whose lifetime is the entire program; stack allocation for arrays whose lifetime is an invocation of a subroutine; heap allocation for dynamically allocated arrays with more general lifetime.
对于形状在制定阶段才知道的数组,或者形状在执行过程中可能发生变化的数组,存储管理更为复杂。对于这些数组,编译器不仅必须安排分配空间,而且还必须在运行时提供形状信息(如果没有这些信息,索引就不可能实现)。一些动态类型语言允许运行时绑定维数和维数边界都是如此。编译型语言可能允许边界是动态的,但通常要求维数是静态的。在阐述时已知形状的本地数组仍可能分配在堆栈中。在执行过程中大小可能发生变化的数组通常必须分配在堆中。
Storage management is more complex for arrays whose shape is not known until elaboration time, or whose shape may change during execution. For these the compiler must arrange not only to allocate space, but also to make shape information available at run time (without such information, indexing would not be possible). Some dynamically typed languages allow run-time binding of both the number and bounds of dimensions. Compiled languages may allow the bounds to be dynamic, but typically require the number of dimensions to be static. A local array whose shape is known at elaboration time may still be allocated in the stack. An array whose size may change during execution must generally be allocated in the heap.
在下面的第一小节中,我们考虑用于在运行时保存形状信息的描述符或内幕向量3。然后,我们分别考虑动态形状数组的基于堆栈和基于堆的分配。
In the first subsection below we consider the descriptors, or dope vectors,3 used to hold shape information at run time. We then consider stack- and heap-based allocation, respectively, for dynamic shape arrays.
在编译期间,符号表会维护程序中每个数组的维度和边界信息。对于每条记录,它都会维护每个字段的偏移量。当数组维度的数量和边界是静态已知时,编译器可以在符号表中查找它们,以便计算数组元素的地址。当这些值不是静态已知时,编译器必须生成代码以在运行时在原始向量中查找它们。
During compilation, the symbol table maintains dimension and bounds information for every array in the program. For every record, it maintains the offset of every field. When the number and bounds of array dimensions are statically known, the compiler can look them up in the symbol table in order to compute the address of elements of the array. When these values are not statically known, the compiler must generate code to look them up in a dope vector at run time.
在一般情况下,dope 向量必须指定每个维度的下限和除最后一个维度之外的每个维度的大小(最后一个维度始终是元素类型的大小,因此是静态已知的)。如果语言实现对数组引用中的越界下标执行动态语义检查,则 dope 向量也可能包含上限。给定下限和大小,上限信息是多余的,但包括它可以避免在运行时重复重新计算。
In the general case a dope vector must specify the lower bound of each dimension and the size of each dimension other than the last (which is always the size of the element type, and will thus be statically known). If the language implementation performs dynamic semantic checks for out-of-bounds subscripts in array references, then the dope vector may contain upper bounds as well. Given lower bounds and sizes, the upper bound information is redundant, but including it avoids the need to recompute repeatedly at run time.
内幕向量的内容在精化时或维度数量或边界发生变化时初始化。在 Fortran 90 等语言中,其形状概念包括维度大小但不包括下限,赋值语句可能不仅需要复制数组的数据,还需要复制内幕向量的内容。
The contents of the dope vector are initialized at elaboration time, or whenever the number or bounds of dimensions change. In a language like Fortran 90, whose notion of shape includes dimension sizes but not lower bounds, an assignment statement may need to copy not only the data of an array, but dope vector contents as well.
在同时提供变量值模型和动态形状数组的语言中,我们必须考虑记录可能包含大小不为静态所知的字段的可能性。在这种情况下,编译器可能不仅将内幕向量用于动态形状数组,还将其用于动态形状记录。记录的内幕向量通常指示每个字段与记录开头的偏移量。
In a language that provides both a value model of variables and arrays of dynamic shape, we must consider the possibility that a record will contain a field whose size is not statically known. In this case the compiler may use dope vectors not only for dynamic shape arrays, but also for dynamic shape records. The dope vector for a record typically indicates the offset of each field from the beginning of the record.
子程序参数和局部变量是动态形状数组最简单的例子。早期版本的 Pascal 要求静态指定所有数组的形状。标准 Pascal 允许动态数组作为子程序参数,其形状在子程序调用时固定。此类参数有时称为一致数组。 除其他外,它们还有助于构建线性代数库,这些库的例程通常必须在任意大小的数组上工作。为了实现这样的数组,编译器安排调用者传递数组的数据和适当的 dope 向量。如果数组在调用者的上下文中是动态形状,则 dope 向量可能已经可用。如果数组在调用者的上下文中是静态形状,则需要在调用之前创建适当的 dope 向量。
Subroutine parameters and local variables provide the simplest examples of dynamic shape arrays. Early versions of Pascal required the shape of all arrays to be specified statically. Standard Pascal allowed dynamic arrays as subroutine parameters, with shape fixed at subroutine call time. Such parameters are sometimes known as conformant arrays. Among other things, they facilitate the construction of linear algebra libraries, whose routines must typically work on arrays of arbitrary size. To implement such an array, the compiler arranges for the caller to pass both the data of the array and an appropriate dope vector. If the array is of dynamic shape in the caller's context, the dope vector may already be available. If the array is of static shape in the caller's context, an appropriate dope vector will need to be created prior to the call.
可以随时改变形状的数组有时被称为完全动态的。由于大小变化通常不按 FIFO 顺序发生,因此堆栈分配不够;完全动态数组必须在堆中分配。
Arrays that can change shape at arbitrary times are sometimes said to be fully dynamic. Because changes in size do not in general occur in FIFO order, stack allocation will not suffice; fully dynamic arrays must be allocated in the heap.
动态可调整大小的数组(字符串除外)出现在 APL、Common Lisp 和各种脚本语言中。它们也分别受C++、Java 和 C# 库的vector、Vector和ArrayList类的支持。与 Fortran 90 的可分配数组相比,这些数组可以改变其形状(特别是可以增长),同时保留其当前内容。在许多情况下,增加大小将需要运行时系统分配更大的块,将要保留的任何数据从旧块复制到新块,然后释放旧块。
Dynamically resizable arrays (other than strings) appear in APL, Common Lisp, and the various scripting languages. They are also supported by the vector, Vector, and ArrayList classes of the C++, Java, and C# libraries, respectively. In contrast to the allocate-able arrays of Fortran 90, these arrays can change their shape—in particular, can grow—while retaining their current content. In many cases, increasing the size will require that the run-time system allocate a larger block, copy any data that are to be retained from the old block to the new, and then deallocate the old.
如果完全动态数组的维数是静态已知的,则可以将内部向量与指向数据的指针一起保存在声明数组的子例程的堆栈框架中。如果维数可以改变,则通常必须将内部向量放置在堆块的开头。
If the number of dimensions of a fully dynamic array is statically known, the dope vector can be kept, together with a pointer to the data, in the stack frame of the subroutine in which the array was declared. If the number of dimensions can change, the dope vector must generally be placed at the beginning of the heap block instead.
在没有垃圾收集的情况下,编译器必须在控制从声明全动态数组的子例程返回时安排回收全动态数组占用的空间。堆栈分配的数组的空间当然会通过弹出堆栈自动回收。
In the absence of garbage collection, the compiler must arrange to reclaim the space occupied by fully dynamic arrays when control returns from the subroutine in which they were declared. Space for stack-allocated arrays is of course reclaimed automatically by popping the stack.
大多数语言实现中的数组都存储在内存中的连续位置。在一维数组中,数组的第二个元素紧跟在第一个元素之后存储;第三个元素紧跟在第二个元素之后存储,依此类推。对于记录数组,对齐约束可能会导致连续元素之间出现小空洞。
Arrays in most language implementations are stored in contiguous locations in memory. In a one-dimensional array, the second element of the array is stored immediately after the first; the third is stored immediately after the second, and so forth. For arrays of records, alignment constraints may result in small holes between consecutive elements.
有些语言对某些数组采用连续分配的替代方法。它们不要求数组的行相邻,而是允许它们位于内存中的任何位置,并创建指向行的辅助指针数组。如果数组有超过两个维度,则可以将其分配为指向指针数组的指针数组……这种行指针内存布局需要更多空间在大多数情况下,但有三个潜在的优势。第一个仅具有历史意义:在 1980 年之前设计的机器上,行指针布局有时会导致更快的代码(参见下面有关地址计算的讨论)。其次,行指针布局允许行具有不同的长度,而不必在行末尾留出空间来留出空洞。这种表示有时称为不规则数组。缺少空洞有时可能会抵消指针的增加空间。第三,行指针布局允许程序从预先存在的行(可能分散在整个内存中)构造数组而无需复制。C、C++ 和 C# 为多维数组提供了连续和行指针组织。从技术上讲,连续布局是一个真正的多维数组,而行指针布局是一个指向数组的指针数组。Java 对所有数组都使用行指针布局。
Some languages employ an alternative to contiguous allocation for some arrays. Rather than require the rows of an array to be adjacent, they allow them to lie anywhere in memory, and create an auxiliary array of pointers to the rows. If the array has more than two dimensions, it may be allocated as an array of pointers to arrays of pointers to…. This row-pointer memory layout requires more space in most cases, but has three potential advantages. The first is of historical interest only: on machines designed before about 1980, row-pointer layout sometimes led to faster code (see the discussion of address calculations below). Second, row-pointer layout allows the rows to have different lengths, without devoting space to holes at the ends of the rows. This representation is sometimes called a ragged array. The lack of holes may sometimes offset the increased space for pointers. Third, row-pointer layout allows a program to construct an array from preexisting rows (possibly scattered throughout memory) without copying. C, C++, and C# provide both contiguous and row-pointer organizations for multidimensional arrays. Technically speaking, the contiguous layout is a true multidimensional array, while the row-pointer layout is an array of pointers to arrays. Java uses the row-pointer layout for all arrays.
在所有示例中,我们都忽略了对越界下标的动态语义检查问题。我们将在练习 8.10中探讨这些代码。在 C-17.5.2 节中,我们将考虑可用于静态消除许多检查的代码改进技术,特别是在枚举控制循环中。
In all our examples, we have ignored the issue of dynamic semantic checks for out-of-bound subscripts. We explore the code for these in Exercise 8.10. In Section C-17.5.2 we will consider code improvement techniques that can be used to eliminate many checks statically, particularly in enumeration-controlled loops.
在某些语言中,字符串仅仅是一个字符数组。在其他语言中,字符串具有特殊地位,其操作无法用于其他类型的数组。Perl、Python 和 Ruby 等脚本语言具有大量内置字符串运算符和函数,包括基于正则表达式的复杂模式匹配功能。某些专用语言(尤其是 Icon)提供更复杂的机制,包括通用生成器和回溯搜索。我们将在第 14.4.2 节中更详细地考虑脚本语言的字符串和模式匹配功能。Icon 已在第 C-6.5.4 节中讨论。在本节的剩余部分,我们将重点讨论字符串在更传统的语言中的作用。
In some languages, a string is simply an array of characters. In other languages, strings have special status, with operations that are not available for arrays of other sorts. Scripting languages like Perl, Python, and Ruby have extensive suites of built-in string operators and functions, including sophisticated pattern matching facilities based on regular expressions. Some special-purpose languages—Icon, in particular—provide even more sophisticated mechanisms, including general-purpose generators and backtracking search. We will consider the string and pattern-matching facilities of scripting languages in more detail in Section 14.4.2. Icon was discussed in Section C-6.5.4. In the remainder of the current section we focus on the role of strings in more traditional languages.
几乎所有编程语言都允许将文字字符串指定为字符序列,通常用单引号或双引号括起来。大多数语言区分文字字符(通常用单引号分隔)和文字字符串(通常用双引号分隔)。少数语言不做这样的区分,将字符定义为长度为 1 的字符串。大多数语言还提供转义序列,允许非打印字符和引号出现在文字字符串内。
Almost all programming languages allow literal strings to be specified as a sequence of characters, usually enclosed in single or double quote marks. Most languages distinguish between literal characters (often delimited with single quotes) and literal strings (often delimited with double quotes). A few languages make no such distinction, defining a character as simply a string of length one. Most languages also provide escape sequences that allow nonprinting characters and quote marks to appear inside literal strings.
为字符串提供的操作集与语言设计者设想的实现密切相关。一些通常不允许数组动态更改大小的语言为字符串提供了这种灵活性。理由有两方面。首先,对可变长度字符串的操作是大量计算机应用程序的基础,在某种意义上“值得”特殊对待。其次,字符串是一维的,具有单字节元素,并且从不包含对其他任何内容的引用,这使动态大小字符串比一般动态数组更容易实现。
The set of operations provided for strings is strongly tied to the implementation envisioned by the language designer(s). Several languages that do not in general allow arrays to change size dynamically do provide this flexibility for strings. The rationale is twofold. First, manipulation of variable-length strings is fundamental to a huge number of computer applications, and in some sense “deserves” special treatment. Second, the fact that strings are one-dimensional, have one-byte elements, and never contain references to anything else makes dynamic-size strings easier to implement than general dynamic arrays.
其他语言允许字符串值变量的长度在其生命周期内发生变化,要求将变量实现为堆中的块或块链。ML 和 Lisp 将字符串作为内置类型提供。C++、Java 和 C# 将它们作为预定义的对象类提供,以正式的面向对象意义提供。在所有这些语言中,字符串变量都是对字符串的引用。为这样的变量分配新值会使其引用不同的对象 - 每个这样的对象都是不可变的。连接和其他字符串运算符会隐式创建新对象。不再可从任何变量访问的对象所使用的空间将被自动回收。
Other languages allow the length of a string-valued variable to change over its lifetime, requiring that the variable be implemented as a block or chain of blocks in the heap. ML and Lisp provide strings as a built-in type. C++, Java, and C# provide them as predefined classes of object, in the formal, object-oriented sense. In all these languages a string variable is a reference to a string. Assigning a new value to such a variable makes it refer to a different object—each such object is immutable. Concatenation and other string operators implicitly create new objects. The space used by objects that are no longer reachable from any variable is reclaimed automatically.
不幸的是,位向量对于大型基类型不太适用:一组整数(表示为位向量)在 32 位机器上会占用大约 500 兆字节。对于 64 位整数,位向量集占用的内存比目前世界上所有计算机所包含的内存还要多。由于这个问题,某些语言(包括早期版本的 Pascal,但不是 ISO 标准)将集合限制为少于某个固定值数量的基类型。
Unfortunately, bit vectors do not work well for large base types: a set of integers, represented as a bit vector, would consume some 500 megabytes on a 32-bit machine. With 64-bit integers, a bit-vector set would consume more memory than is currently contained on all the computers in the world. Because of this problem, some languages (including early versions of Pascal, though not the ISO standard) limited sets to base types of fewer than some fixed number of values.
对于从大集合中抽取的元素集合,大多数现代语言使用替代实现,其大小与现有元素的数量成正比,而不是与基类型中的值的数量成正比。大多数语言还提供了内置迭代器(第 6.5.3 节)来生成集合的元素。通常会区分有序列表和无序列表,有序列表的基类型必须支持某种排序概念,其迭代器按从小到大的顺序生成元素,无序列表的迭代器按任意顺序生成元素。有序集通常用跳跃列表或各种树来实现。无序列表通常用哈希表来实现。
For sets of elements drawn from a large universe, most modern languages use alternative implementations, whose size is proportional to the number of elements present, rather than to the number of values in the base type. Most languages also provide a built-in iterator (Section 6.5.3) to yield the elements of the set. A distinction is often made between sorted lists, whose base type must support some notion of ordering, and whose iterators yield the elements smallest-to-largest, and unordered lists, whose iterators yield the elements in arbitrary order. Ordered sets are commonly implemented with skip lists or various sorts of trees. Unordered sets are commonly implemented with hash tables.
递归类型是指其对象可能包含对该类型的其他对象的一个或多个引用的类型。大多数递归类型都是记录,因为它们需要包含除引用之外的其他内容,这意味着存在异构字段。递归类型用于构建各种“链接”数据结构,包括列表和树。
A recursive type is one whose objects may contain one or more references to other objects of the type. Most recursive types are records, since they need to contain something in addition to the reference, implying the existence of heterogeneous fields. Recursive types are used to build a wide variety of “linked” data structures, including lists and trees.
在使用变量引用模型的语言中,类型为foo的记录很容易包含对类型为foo的另一个记录的引用:每个变量(因此每个记录字段)都是引用。在使用变量值模型的语言中,递归类型需要指针的概念:一个变量(或字段),其值是对某个对象的引用。指针最早是在 PL/I 中引入的。
In languages that use a reference model of variables, it is easy for a record of type foo to include a reference to another record of type foo: every variable (and hence every record field) is a reference anyway. In languages that use a value model of variables, recursive types require the notion of a pointer: a variable (or field) whose value is a reference to some object. Pointers were first introduced in PL/I.
在某些语言(例如 Pascal、Modula-3 和 Ada 83)中,指针被限制为仅指向堆中的对象。创建新指针值的唯一方法(不使用变体记录或强制类型转换来绕过类型系统)是调用内置函数,该函数在堆中分配一个新对象并返回指向该对象的指针。在其他语言(无论是新语言还是旧语言)中,都可以使用“地址”运算符创建指向非堆对象的指针。我们将在下面的第一小节中更详细地研究指针操作以及引用和值模型的影响。
In some languages (e.g., Pascal, Modula-3, and Ada 83), pointers were restricted to point only to objects in the heap. The only way to create a new pointer value (without using variant records or casts to bypass the type system) was to call a built-in function that allocated a new object in the heap and returned a pointer to it. In other languages, both old and new, one can create a pointer to a nonheap object by using an “address of” operator. We will examine pointer operations and the ramifications of the reference and value models in more detail in the first subsection below.
在任何允许从堆中分配新对象的语言中,都会出现一个问题:如何以及何时回收不再需要的对象的存储空间?在短期程序中,简单地让存储空间闲置是可以接受的,但在大多数情况下,必须回收未使用的空间,以便为其他内容腾出空间。无法回收不再需要的对象空间的程序被称为“内存泄漏”。如果这样的程序运行时间过长,可能会耗尽空间并崩溃。
In any language that permits new objects to be allocated from the heap, the question arises: how and when is storage reclaimed for objects that are no longer needed? In short-lived programs it may be acceptable simply to leave the storage unused, but in most cases unused space must be reclaimed, to make room for other things. A program that fails to reclaim the space for objects that are no longer needed is said to “leak memory.” If such a program runs for an extended period of time, it may run out of space and crash.
某些语言(包括 C、C++ 和 Rust)要求程序员明确回收空间。其他语言(包括 Java、C#、Scala、Go 以及所有函数式和脚本语言)要求语言实现自动回收未使用的对象。显式存储回收简化了语言实现,但增加了程序员忘记回收不再活动的对象(从而泄漏内存)或意外回收仍在使用的对象(从而创建悬空引用)的可能性。自动存储回收(也称为垃圾收集)大大简化了程序员的任务,但会带来一定的运行时成本,并且引发了语言实现如何区分垃圾和活动对象的问题。我们将分别在第 8.5.2 节和第 8.5.3节中进一步讨论悬空引用和垃圾收集。
Some languages, including C, C++, and Rust, require the programmer to reclaim space explicitly. Other languages, including Java, C#, Scala, Go, and all the functional and scripting languages, require the language implementation to reclaim unused objects automatically. Explicit storage reclamation simplifies the language implementation, but raises the possibility that the programmer will forget to reclaim objects that are no longer live (thereby leaking memory), or will accidentally reclaim objects that are still in use (thereby creating dangling references). Automatic storage reclamation (otherwise known as garbage collection) dramatically simplifies the programmer's task, but imposes certain runtime costs, and raises the question of how the language implementation is to distinguish garbage from active objects. We will discuss dangling references and garbage collection further in Sections 8.5.2 and 8.5.3, respectively.
指针上的操作包括堆中对象的分配和释放、指针的取消引用以访问其指向的对象以及赋值从一个指针到另一个指针的转换。这些操作的行为很大程度上取决于语言是函数式的还是命令式的,以及它对变量/名称采用的是引用模型还是值模型。
Operations on pointers include allocation and deallocation ofobjects in the heap, dereferencing of pointers to access the objects to which they point, and assignment of one pointer into another. The behavior of these operations depends heavily on whether the language is functional or imperative, and on whether it employs a reference or value model for variables/names.
函数式语言通常采用引用模型来命名(纯函数式语言没有变量或赋值)。函数式语言中的对象往往根据需要自动分配,其结构由语言实现决定。命令式语言中的变量可以使用值模型或引用模型,或者两者的组合。在使用值模型的 C 或 Ada 中,赋值A = B会将B的值放入A中。如果我们想要B引用一个对象,并且想要A = B让A引用B引用的对象,那么A和B必须是指针。在使用引用模型的 Smalltalk 或 Ruby 中,赋值A = B总是让A引用B引用的同一个对象。
Functional languages generally employ a reference model for names (a purely functional language has no variables or assignments). Objects in a functional language tend to be allocated automatically as needed, with a structure determined by the language implementation. Variables in an imperative language may use either a value or a reference model, or some combination of the two. In C or Ada, which employ a value model, the assignment A = B puts the value of B into A. If we want B to refer to an object, and we want A = B to make A refer to the object to which B refers, then A and B must be pointers. In Smalltalk or Ruby, which employ a reference model, the assignment A = B always makes A refer to the same object to which B refers.
Java 走的是中间路线,其中引用模型的通常实现在语言语义中明确说明。内置 Java 类型(整数、浮点数、字符和布尔值)的变量采用值模型;用户定义类型(字符串、数组和面向对象意义上的其他对象)的变量采用引用模型。如果 A 和 B 是内置类型,则 Java 中的赋值A = B会将B的值放入A ;如果A和B是用户定义类型,则它使A引用B所引用的对象。默认情况下,C# 镜像 Java,但其他语言功能(明确标记为“不安全”)允许系统程序员在需要时使用指针。
Java charts an intermediate course, in which the usual implementation of the reference model is made explicit in the language semantics. Variables of built-in Java types (integers, floating-point numbers, characters, and Booleans) employ a value model; variables of user-defined types (strings, arrays, and other objects in the object-oriented sense of the word) employ a reference model. The assignment A = B in Java places the value of B into A if A and B are of built-in type; it makes A refer to the object to which B refers if A and B are of user-defined type. C# mirrors Java by default, but additional language features, explicitly labeled “unsafe,” allow systems programmers to use pointers when desired.
如果在 ML 或 Lisp 中以纯函数式风格编程,则使用递归类型创建的数据结构将变成非循环的。新对象引用旧对象,但旧对象永远不会改变,因此永远不会指向新对象。循环结构通常使用语言的命令式特征来定义。(有关此规则的例外情况,请参见练习 8.21。)在 ML 中,命令式特征包括显式指针概念,下面在“值模型”中简要讨论。
If one programs in a purely functional style in ML or in Lisp, the data structures created with recursive types turn out to be acyclic. New objects refer to old ones, but old ones never change, and thus never point to new ones. Circular structures are typically defined by using the imperative features of the languages. (For an exception to this rule, see Exercise 8.21.) In ML, the imperative features include an explicit notion of pointer, discussed briefly under “Value Model” below.
在所有情况下,声明必须允许编译器(或人类读者)确定数组元素的大小,或者换句话说,指针引用的对象的大小。因此,int a[][]和int (*a)[]都不是有效的变量或参数声明:它们都没有为编译器提供生成a + i 或 a[i]代码所需的大小信息。
In all cases, a declaration must allow the compiler (or human reader) to determine the size of the elements of an array or, equivalently, the size of the objects referred to by a pointer. Thus neither int a[][] nor int (*a)[] is a valid variable or parameter declaration: neither provides the compiler with the size information it needs to generate code for a + i or a[i].
由于语言实现可能会重用已回收的堆栈和堆对象的空间,因此使用悬垂引用的程序可能会读取或写入内存中现已属于其他对象的位。它甚至可能会修改现已属于实现的簿记信息的位,从而破坏堆栈或堆的结构。
Because a language implementation may reuse the space of reclaimed stack and heap objects, a program that uses a dangling reference may read or write bits in memory that are now part of some other object. It may even modify bits that are now part of the implementation's bookkeeping information, corrupting the structure of the stack or heap.
Algol 68 通过禁止指针指向任何生存期比指针本身更短的对象来解决对堆栈对象的悬垂引用问题。不幸的是,这条规则很难执行。除其他事项外,由于指针和指针可能引用的对象都可以作为参数传递给子例程,因此只有当引用参数附带隐藏的生存期指示时,动态语义检查才有可能。Ada 有一个更严格的规则,更容易执行:它禁止指针指向任何生存期比指针类型的生存期更短的对象。
Algol 68 addressed the problem of dangling references to stack objects by forbidding a pointer from pointing to any object whose lifetime was briefer than that of the pointer itself. Unfortunately, this rule is difficult to enforce. Among other things, since both pointers and objects to which pointers might refer can be passed as arguments to subroutines, dynamic semantic checks are possible only if reference parameters are accompanied by a hidden indication of lifetime. Ada has a more restrictive rule that is easier to enforce: it forbids a pointer from pointing to any object whose lifetime is briefer than that of the pointer's type.
更深入地
IN MORE DEPTH
在配套网站上,我们考虑了两种有时用于在运行时捕获悬空引用的机制。墓碑在每次指针访问时引入了额外的间接层。回收对象时,间接字(墓碑)将被标记,以使对该对象的未来引用无效。锁和钥匙会向堆中的每个指针和每个对象添加一个字;这些字必须匹配,指针才有效。墓碑可用于允许指向非堆对象的指针的语言,但它们引入了回收墓碑本身的第二个问题。锁和钥匙稍微简单一些,但它们只适用于堆中的对象。
On the companion site we consider two mechanisms that are sometimes used to catch dangling references at run time. Tombstones introduce an extra level of indirection on every pointer access. When an object is reclaimed, the indirection word (tombstone) is marked in a way that invalidates future references to the object. Locks and keys add a word to every pointer and to every object in the heap; these words must match for the pointer to be valid. Tombstones can be used in languages that permit pointers to nonheap objects, but they introduce the secondary problem of reclaiming the tombstones themselves. Locks and keys are somewhat simpler, but they work only for objects in the heap.
显式回收堆对象对程序员来说是一个沉重的负担,也是导致错误(内存泄漏和悬空引用)的主要来源。跟踪对象生存期所需的代码使程序的设计、实现和维护更加困难。一个有吸引力的替代方案是让语言实现通知对象何时不再有用并自动回收它们。自动回收(也称为垃圾收集)对于函数式语言或多或少是必不可少的:删除是一种非常命令式的操作,并且从函数构造和返回任意对象的能力意味着许多在命令式语言中在堆栈上分配的对象必须从函数式语言中的堆中分配,以赋予它们无限的范围。
Explicit reclamation of heap objects is a serious burden on the programmer and a major source of bugs (memory leaks and dangling references). The code required to keep track of object lifetimes makes programs more difficult to design, implement, and maintain. An attractive alternative is to have the language implementation notice when objects are no longer useful and reclaim them automatically. Automatic reclamation (otherwise known as garbage collection) is more or less essential for functional languages: delete is a very imperative sort of operation, and the ability to construct and return arbitrary objects from functions means that many objects that would be allocated on the stack in an imperative language must be allocated from the heap in a functional language, to give them unlimited extent.
随着时间的推移,自动垃圾收集在命令式语言中也变得流行起来。Java、C#、Scala、Go 和所有主流脚本语言中都有这种功能。自动收集很难实现,但与实现后程序员享受的便利相比,这种困难微不足道。自动收集也往往比手动回收慢,尽管它消除了检查悬空引用的需要。
Over time, automatic garbage collection has become popular for imperative languages as well. It can be found in, among others, Java, C#, Scala, Go, and all the major scripting languages. Automatic collection is difficult to implement, but the difficulty pales in comparison to the convenience enjoyed by programmers once the implementation exists. Automatic collection also tends to be slower than manual reclamation, though it eliminates any need to check for dangling references.
什么时候对象不再有用?一个可能的答案是:当没有指向它的指针时。6最简单的垃圾收集技术只是在每个对象中放置一个计数器,以跟踪引用该对象的指针数量。创建对象时,此引用计数设置为 1,以表示指针由新操作返回。当一个指针被赋值给另一个指针时,运行时系统会减少赋值左侧先前引用的对象(如果有)的引用计数,并增加右侧引用的对象的计数。在子程序返回时,调用序列结尾必须减少即将被销毁的本地指针引用的任何对象的引用计数。当引用计数达到零时,可以回收其对象。递归地,运行时系统必须减少被回收对象内指针引用的任何对象的计数,如果它们的计数达到零,则回收这些对象。为了防止收集器跟踪垃圾地址,每个指针必须在阐述时初始化为空。
When is an object no longer useful? One possible answer is: when no pointers to it exist.6 The simplest garbage collection technique simply places a counter in each object that keeps track of the number of pointers that refer to the object. When the object is created, this reference count is set to one, to represent the pointer returned by the new operation. When one pointer is assigned into another, the run-time system decrements the reference count of the object (if any) formerly referred to by the assignment's left-hand side, and increments the count of the object referred to by the right-hand side. On subroutine return, the calling sequence epilogue must decrement the reference count of any object referred to by a local pointer that is about to be destroyed. When a reference count reaches zero, its object can be reclaimed. Recursively, the run-time system must decrement counts for any objects referred to by pointers within the object being reclaimed, and reclaim those objects if their counts reach zero. To prevent the collector from following garbage addresses, each pointer must be initialized to null at elaboration time.
为了使引用计数发挥作用,语言实现必须能够识别每个指针的位置。当子程序返回时,它必须能够分辨出堆栈框架中的哪些字代表指针;当堆中的对象被回收时,它必须能够分辨出对象中的哪些字代表指针。跟踪此信息的标准技术依赖于编译器生成的类型描述符。程序中每个不同类型都有一个描述符,每个子程序的堆栈框架都有一个描述符,全局变量集也有一个描述符。大多数描述符只是一个表,列出了可以找到指针的类型内的偏移量,以及这些指针引用的对象类型的描述符地址。对于标记变体记录(可区分联合)类型,描述符稍微复杂一些:它必须包含标记的值(或范围)列表,以及相应变体的表。对于未标记的变体记录,没有可接受的解决方案:引用计数仅在语言是强类型时才有效(但请参阅第 8.5.3 节末尾的“保守收集”的讨论)。
In order for reference counts to work, the language implementation must be able to identify the location of every pointer. When a subroutine returns, it must be able to tell which words in the stack frame represent pointers; when an object in the heap is reclaimed, it must be able to tell which words within the object represent pointers. The standard technique to track this information relies on type descriptors generated by the compiler. There is one descriptor for every distinct type in the program, plus one for the stack frame of each subroutine, and one for the set of global variables. Most descriptors are simply a table that lists the offsets within the type at which pointers can be found, together with the addresses of descriptors for the types of the objects referred to by those pointers. For a tagged variant record (discriminated union) type, the descriptor is a bit more complicated: it must contain a list of values (or ranges) for the tag, together with a table for the corresponding variant. For untagged variant records, there is no acceptable solution: reference counts work only if the language is strongly typed (but see the discussion of “Conservative Collection” at the end of Section 8.5.3).
通用术语“智能指针”是指程序级对象(在语言本身之上实现),它模仿指针的行为,但具有额外的语义。智能指针最常见的用途是在通常仅支持手动存储回收的语言中实现引用计数。其他用途包括指针算法的边界检查、调试或性能分析的检测以及对外部对象(例如打开的文件)的引用的跟踪。
The general term smart pointer refers to a program-level object (implemented on top of the language proper) that mimics the behavior of a pointer, but with additional semantics. The most common use of smart pointers is to implement reference counting in a language that normally supports only manual storage reclamation. Other uses include bounds checking on pointer arithmetic, instrumentation for debugging or performance analysis, and tracking of references to external objects—e.g., open files.
在 C++ 标准库中可以找到对智能指针的特别丰富的支持,其unique_ptr、shared_ptr和weak_ptr类利用运算符重载、构造函数、析构函数和移动语义来简化原本困难的手动回收任务。unique_ptr顾名思义就是对象的唯一引用。如果unique_ptr被销毁(通常是因为声明它的函数返回),那么它指向的对象将被指针的析构函数回收,如第8.5.2 节中所述。如果将一个unique_ptr赋值给另一个 unique_ptr(或作为参数传递),则重载的赋值运算符或构造函数会通过将旧指针更改为null来转移指向对象的所有权。(移动语义,我们将在第 9.3.1 节的“C++ 中的引用”中更详细地描述,通常允许编译器优化所有权转移的成本。)
Particularly rich support for smart pointers can be found in the C++ standard library, whose unique_ptr, shared_ptr, and weak_ptr classes leverage operator overloading, constructors, destructors, and move semantics to simplify the otherwise difficult task of manual reclamation. A unique_ptr is what its name implies—the only reference to an object. If the unique_ptr is destroyed (typically because the function in which it was declared returns), then the object to which it points is reclaimed by the pointer's destructor, as suggested in Section 8.5.2. If one unique_ptr is assigned into another (or passed as a parameter), the overloaded assignment operator or constructor transfers ownership of the pointed-to object by changing the old pointer to null. (Move semantics, which we will describe in more detail in under “References in C++” in Section 9.3.1, often allow the compiler to optimize away the cost of the ownership transfer.)
shared_ptr类型为指向的对象实现引用计数,通常将其存储在隐藏的、类似墓碑的中间对象中。计数在shared_ptr构造函数中递增,在析构函数中递减,并调整(双向)通过赋值操作。当需要循环结构时,或者当程序员想要维护簿记信息而又不想人为延长对象生命周期时,可以使用weak_ptr指向对象而不参与引用计数。当没有指向该对象的shared_ptr时,C++库将回收该对象;任何剩余的weak_ptr随后将表现得像它们为null一样。
The shared_ptr type implements a reference count for the pointed-to object, typically storing it in a hidden, tombstone-like intermediate object. Counts are incremented in shared_ptr constructors, decremented in destructors, and adjusted (in both directions) by assignment operations. When circular structures are required, or when the programmer wants to maintain bookkeeping information without artificially extending object lifetimes, a weak_ptr can be used to point to an object without contributing to reference counting. The C++ library will reclaim an object when no shared_ptr to it remains; any remaining weak_ptrs will subsequently behave as if they were null.
正如我们所见,引用计数将对象定义为有用的,只要存在指向它的指针。更好的定义可能是说,如果可以通过从具有名称的某个对象(即堆外的某个对象)开始的一系列有效指针到达某个对象,则该对象是有用的。根据此定义,图 8.14下半部分中的块是无用的,即使它们的引用计数非零。跟踪收集器通过从外部指针开始递归探索堆来确定什么是有用的。
As we have seen, reference counting defines an object to be useful if there exists a pointer to it. A better definition might say that an object is useful if it can be reached by following a chain of valid pointers starting from something that has a name (i.e., something outside the heap). According to this definition, the blocks in the bottom half of Figure 8.14 are useless, even though their reference counts are nonzero. Tracing collectors work by recursively exploring the heap, starting from external pointers, to determine what is useful.
根据这个更准确的定义,识别无用块的经典机制称为标记和清除。它分为三个主要步骤,当堆中剩余的可用空间量低于某个最低阈值时,由垃圾收集器执行:
The classic mechanism to identify useless blocks, under this more accurate definition, is known as mark-and-sweep. It proceeds in three main steps, executed by the garbage collector when the amount of free space remaining in the heap falls below some minimum threshold:
1. The collector walks through the heap, tentatively marking every block as “useless.”
2. 从堆外的所有指针开始,收集器以递归方式探索程序中所有链接的数据,将每个新发现的块标记为“有用”。 (当遇到已经标记为“有用”的块时,收集器知道它已经通过先前的路径到达该块,并且无需递归即可返回。)
2. Beginning with all pointers outside the heap, the collector recursively explores all linked data in the program, marking each newly discovered block as “useful.” (When it encounters a block that is already marked as “useful,” the collector knows it has reached the block over some previous path, and returns without recursing.)
3. 收集器再次遍历堆,将每个仍然标记为“无用”的块移到空闲列表中。
3. The collector again walks through the heap, moving every block that is still marked “useless” to the free list.
该算法的几个潜在问题显而易见。首先,无论是初始还是最终遍历堆,都要求收集器能够分辨出每个“正在使用”的块的开始和结束位置。在具有可变大小堆块的语言中,每个块都必须以指示其大小以及当前是否空闲开始。其次,收集器必须能够在步骤 2 中找到每个块中包含的指针。标准解决方案是将指向类型描述符的指针放置在每个块的开头附近。
Several potential problems with this algorithm are immediately apparent. First, both the initial and final walks through the heap require that the collector be able to tell where every “in-use” block begins and ends. In a language with variable-size heap blocks, every block must begin with an indication of its size, and of whether it is currently free. Second, the collector must be able in Step 2 to find the pointers contained within each block. The standard solution is to place a pointer to a type descriptor near the beginning of each block.
在具有可变大小堆块的语言中,垃圾收集器可以通过执行存储压缩来减少外部碎片。许多垃圾收集器采用一种称为停止和复制的技术,该技术在实现压缩的同时消除了标准标记和清除算法中的步骤 1 和 3。具体来说,它们将堆分成两个大小相等的区域。所有分配都发生在前半部分。当这一半(几乎)已满时,收集器开始探索可到达的数据结构。每个可到达的块都被复制到堆后半部分的连续位置,没有外部碎片。堆前半部分中块的旧版本被“有用”标志和指向新位置的指针覆盖。指向同一块的任何其他指针(并在探索的后期找到)都被设置为指向新位置。当收集器完成探索时,所有有用的对象都已移动(并压缩)到后半部分堆的一半,前半部分不再需要。因此,收集器可以交换前半部分和后半部分的概念,程序可以继续。显然,这种算法的缺点是,在任何给定时间只能使用一半的堆,但在具有虚拟内存的系统中,只有虚拟空间未得到充分利用;堆的每个“一半”可以根据需要占用大部分物理内存。此外,通过消除标准标记和清除的第 1 步和第 3 步,停止和复制产生的开销与非垃圾块的数量成比例,而不是与总块数成比例。
In a language with variable-size heap blocks, the garbage collector can reduce external fragmentation by performing storage compaction. Many garbage collectors employ a technique known as stop-and-copy that achieves compaction while simultaneously eliminating Steps 1 and 3 in the standard mark-and-sweep algorithm. Specifically, they divide the heap into two regions of equal size. All allocation happens in the first half. When this half is (nearly) full, the collector begins its exploration of reachable data structures. Each reachable block is copied into contiguous locations in the second half of the heap, with no external fragmentation. The old version of the block, in the first half of the heap, is overwritten with a “useful” flag and a pointer to the new location. Any other pointer that refers to the same block (and is found later in the exploration) is set to point to the new location. When the collector finishes its exploration, all useful objects have been moved (and compacted) into the second half of the heap, and nothing in the first half is needed anymore. The collector can therefore swap its notion of first and second halves, and the program can continue. Obviously, this algorithm suffers from the fact that only half of the heap can be used at any given time, but in a system with virtual memory it is only the virtual space that is underutilized; each “half” of the heap can occupy most of physical memory as needed. Moreover, by eliminating Steps 1 and 3 of standard mark-and-sweep, stop-and-copy incurs overhead proportional to the number of nongarbage blocks, rather than the total number of blocks.
为了进一步降低跟踪收集的成本,一些垃圾收集器采用了“分代”技术,利用了大多数动态分配的对象寿命较短这一现象。堆被分成多个区域(通常是两个)。当空间不足时,收集器首先检查最年轻的区域(“托儿所”),它认为该区域可能包含最高比例的垃圾。只有当收集器无法回收此区域中的足够空间时,它才会检查下一个较旧的区域。为了避免在长期运行的系统中泄漏存储,收集器必须准备好在必要时检查整个堆。但是,在大多数情况下,收集的开销仅与最年轻区域的大小成正比。
To further reduce the cost of tracing collection, some garbage collectors employ a “generational” technique, exploiting the observation that most dynamically allocated objects are short lived. The heap is divided into multiple regions (often two). When space runs low the collector first examines the youngest region (the “nursery”), which it assumes is likely to have the highest proportion of garbage. Only if it is unable to reclaim sufficient space in this region does the collector examine the next-older region. To avoid leaking storage in long-running systems, the collector must be prepared, if necessary, to examine the entire heap. In most cases, however, the overhead of collection will be proportional to the size of the youngest region only.
任何在当前区域中幸存了少量收集(通常为一次)的对象都会被提升(移动)到下一个较旧的区域,其方式类似于停止和复制。当然,追踪托儿所需要将旧对象指向新对象的指针视为探索的外部“根”。同样,提升也需要更新从旧对象到新对象的指针以反映新位置。虽然旧空间到新空间的指针往往很少见,但分代收集器必须能够快速找到它们。在每次指针分配时,编译器都会生成代码来检查新值是否是旧到新指针;如果是,它会将指针添加到收集器可访问的隐藏列表中。这种分配上的检测称为写屏障。8
Any object that survives some small number of collections (often one) in its current region is promoted (moved) to the next older region, in a manner reminiscent of stop-and-copy. Tracing of the nursery requires, of course, that pointers from old objects to new objects we treated as external “roots” of exploration. Promotion likewise requires that pointers from old objects to new objects be updated to reflect the new locations. While old-space-to-new-space pointers tend to be rare, a generational collector must be able to find them all quickly. At each pointer assignment, the compiler generates code to check whether the new value is an old-to-new pointer; if so, it adds the pointer to a hidden list accessible to the collector. This instrumentation on assignments is known as a write barrier.8
语言实现者传统上认为,只有强类型语言才可能实现自动存储回收:引用计数和跟踪收集都要求我们能够找到对象内的指针。如果我们愿意承认某些垃圾不会被回收,那么我们可以实现标记-清除收集,而无需找到指针 [ BW88 ]。关键是要观察到堆中任何给定的块都跨越相对较少的地址。内存中某个非指针字恰好包含类似于其中一个地址的位模式的可能性非常小。
Language implementors have traditionally assumed that automatic storage reclamation is possible only in languages that are strongly typed: both reference counts and tracing collection require that we be able to find the pointers within an object. If we are willing to admit the possibility that some garbage will go unreclaimed, it turns out that we can implement mark-and-sweep collection without being able to find pointers [BW88]. The key is to observe that any given block in the heap spans a relatively small number of addresses. There is only a very small probability that some word in memory that is not a pointer will happen to contain a bit pattern that looks like one of those addresses.
如果我们保守地假设所有似乎指向堆块的东西实际上都是有效指针,那么我们可以继续进行标记和清除收集。当空间不足时,收集器(像往常一样)暂时将堆中的所有块标记为无用。然后,它会扫描堆栈和全局存储中的所有字对齐量。如果这些字中的任何一个似乎包含堆中某个东西的地址,收集器就会将包含该地址的块标记为有用。然后,收集器会递归扫描块中的所有字对齐量,并将在其中找到地址的任何其他块标记为有用。最后(像往常一样),收集器会回收仍然标记为无用的任何块。
If we assume, conservatively, that everything that seems to point into a heap block is in fact a valid pointer, then we can proceed with mark-and-sweep collection. When space runs low, the collector (as usual) tentatively marks all blocks in the heap as useless. It then scans all word-aligned quantities in the stack and in global storage. If any of these words appears to contain the address of something in the heap, the collector marks the block that contains that address as useful. Recursively, the collector then scans all word-aligned quantities in the block, and marks as useful any other blocks whose addresses are found therein. Finally (as usual), the collector reclaims any blocks that are still marked useless.
只要程序员不“隐藏”指针,该算法就是完全安全的(从某种意义上说,它永远不会回收有用的块)。例如,在 C 语言中,如果程序员将指针转换为int,然后将其与常量进行异或,并期望稍后恢复和使用该指针,则收集器不太可能正常运行。除了有时会留下无人认领的垃圾外,保守收集还存在无法执行压缩的问题:收集器永远无法确定应该更改哪些“指针”。
The algorithm is completely safe (in the sense that it never reclaims useful blocks) so long as the programmer never “hides” a pointer. In C, for example, the collector is unlikely to function correctly if the programmer casts a pointer to int and then xors it with a constant, with the expectation of restoring and using the pointer at a later time. In addition to sometimes leaving garbage unclaimed, conservative collection suffers from the inability to perform compaction: the collector can never be sure which “pointers” should be changed.
列表以递归方式定义为空列表或由初始对象(可以是列表或原子)和另一个(较短)列表组成的对。列表非常适合用函数式和逻辑语言进行编程,这些语言的大部分工作都是通过递归和高阶函数完成的(将在11.6 节中介绍)。
A list is defined recursively as either the empty list or a pair consisting of an initial object (which may be either a list or an atom) and another (shorter) list. Lists are ideally suited to programming in functional and logic languages, which do most of their work via recursion and higher-order functions (to be described in Section 11.6).
列表也可以用于命令式程序。一些传统的编译语言(例如 Clu)和大多数现代脚本语言都支持列表的内置类型构造函数。面向对象语言的库类也普遍支持列表,程序员可以使用任何带有记录和指针的语言来构建自己的列表。由于许多标准列表操作往往会产生垃圾,因此列表在具有自动垃圾收集功能的语言中效果最佳。
Lists can also be used in imperative programs. They are supported by built-in type constructors in a few traditional compiled languages (e.g., Clu) and in most modern scripting languages. They are also commonly supported by library classes in object-oriented languages, and programmers can build their own in any language with records and pointers. Since many of the standard list operations tend to generate garbage, lists tend to work best in a language with automatic garbage collection.
ML 和 Lisp 都提供了丰富的内置多态函数来操作任意列表。由于程序在 Lisp 中就是列表,因此 Lisp 必须区分要评估的列表和要“保持原样”的列表。作为结构。为了防止对文字列表进行求值,Lisp 程序员可以引用它:(quote (abcd)),缩写为 ' (ab cd)。要评估内部列表(例如,函数返回的列表),程序员可以将其传递给内置函数eval。在 ML 中,程序不是列表,因此文字列表始终是结构聚合。
Both ML and Lisp provide a wealth of built-in polymorphic functions to manipulate arbitrary lists. Because programs are lists in Lisp, Lisp must distinguish between lists that are to be evaluated and lists that are to be left “as is,” as structures. To prevent a literal list from being evaluated, the Lisp programmer may quote it: (quote (a b c d)), abbreviated '(ab c d). To evaluate an internal list (e.g., one returned by a function), the programmer may pass it to the built-in function eval. In ML, programs are not lists, so a literal list is always a structural aggregate.
ML 和 Lisp 都提供了许多附加的列表函数,包括测试列表是否为空;返回列表的长度;返回列表的第n 个元素,或返回由除前n 个元素之外的所有元素组成的列表;反转列表元素的顺序;在列表中搜索与某些谓词匹配的元素;或将函数应用于列表的每个元素,并将结果作为列表返回。
Both ML and Lisp provide many additional list functions, including ones that test a list to see if it is empty; return the length of a list; return the nth element of a list, or a list consisting of all but the first n elements; reverse the order of the elements of a list; search a list for elements matching some predicate; or apply a function to every element of a list, returning the results as a list.
输入/输出 (I/O) 设施允许程序与外界通信。在讨论这种通信时,通常要区分交互式I/O 和文件 I/O。交互式 I/O 通常意味着与人类用户或物理设备的通信,这些设备与正在运行的程序并行工作,并且它们对程序的输入可能取决于程序先前的输出(例如提示)。文件通常是指操作系统实现的离线存储。文件可能进一步分为临时文件和持久文件。临时文件在单个程序运行期间存在;它们的目的是存储程序可用内存无法容纳的大量信息。持久文件允许程序读取程序开始运行前存在的数据,并写入程序结束后仍将继续存在的数据。
Input/output (I/O) facilities allow a program to communicate with the outside world. In discussing this communication, it is customary to distinguish between interactive I/O and I/O with files. Interactive I/O generally implies communication with human users or physical devices, which work in parallel with the running program, and whose input to the program may depend on earlier output from the program (e.g., prompts). Files generally refer to off-line storage implemented by the operating system. Files maybe further categorized into those that are temporary and those that are persistent. Temporary files exist for the duration of a single program run; their purpose is to store information that is too large to fit in the memory available to the program. Persistent files allow a program to read data that existed before the program began running, and to write data that will continue to exist after the program has ended.
I/O 是语言设计中最困难的方面之一,也是不同语言之间共性最小的方面。有些语言为 I/O提供内置文件数据类型和特殊语法结构。其他语言将 I/O 完全交给库包,这些库包导出(通常不透明的)文件类型和各种输入和输出子例程。语言集成的主要优势是能够使用非子例程调用语法,并执行库例程可能无法使用的操作(例如,对具有不同数量参数的子例程调用进行类型检查)。另一方面,纯基于库的 I/O 方法可能会使语言定义中出现大量“混乱”。
I/O is one of the most difficult aspects of a language to design, and one that displays the least commonality from one language to the next. Some languages provide built-in file data types and special syntactic constructs for I/O. Others relegate I/O entirely to library packages, which export a (usually opaque) file type and a variety of input and output subroutines. The principal advantage of language integration is the ability to employ non-subroutine-call syntax, and to perform operations (e.g., type checking on subroutine calls with varying numbers of parameters) that may not otherwise be available to library routines. A purely library-based approach to I/O, on the other hand, may keep a substantial amount of “clutter” out of the language definition.
更深入地
IN MORE DEPTH
可以在配套网站上找到语言级 I/O 机制的概述。在简要介绍交互式和基于文件的 I/O 之后,我们主要关注文本文件的常见情况。文本文件中的数据以字符形式存储形式,但在读写操作期间可以转换为内部类型或从内部类型转换为内部类型。作为示例,我们考虑 Fortran、Ada、C 和 C++ 的文本 I/O 功能。
An overview of language-level I/O mechanisms can be found on the companion site. After a brief introduction to interactive and file-based I/O, we focus mainly on the common case of text files. The data in a text file are stored in character form, but may be converted to and from internal types during read and write operations. As examples, we consider the text I/O facilities of Fortran, Ada, C, and C++.
本节总结了我们关于语言设计的六个核心章节中的第四章(名称 [来自第一部分]、控制流、类型系统、复合类型、子例程和类)。在我们对复合类型的调查中,我们在记录、数组和递归类型上花费了最多的时间。记录的关键问题包括变体记录的语法和语义、整个记录操作、类型安全以及它们与内存布局的交互。内存布局对于数组也很重要,它与形状的绑定时间、静态、堆栈和基于堆的分配策略、数字应用程序中的高效数组遍历、C 中指针和数组的互操作性以及可用的整个数组和基于切片的操作集交互。
This section concludes the fourth of our six core chapters on language design (names [from Part I], control flow, type systems, composite types, subroutines, and classes). In our survey of composite types, we spent the most time on records, arrays, and recursive types. Key issues for records include the syntax and semantics of variant records, whole-record operations, type safety, and the interaction of each of these with memory layout. Memory layout is also important for arrays, in which it interacts with binding time for shape; static, stack, and heap-based allocation strategies; efficient array traversal in numeric applications; the interoperability of pointers and arrays in C; and the available set of whole-array and slice-based operations.
对于递归数据类型,很大程度上取决于变量/名称的值模型和引用模型之间的选择。递归类型是引用模型的自然产物;对于值模型,它们需要指针的概念:一个值是引用的变量。从实现的角度来看,值和引用之间的区别很重要:将内置类型实现为引用会很浪费,因此具有引用模型的语言通常会以不同的方式实现内置类型和用户定义类型。Java 在语言语义中反映了这种区别,要求内置类型的值模型和用户定义类类型的对象的引用模型。
For recursive data types, much depends on the choice between the value and reference models of variables/names. Recursive types are a natural fallout of the reference model; with the value model they require the notion of a pointer: a variable whose value is a reference. The distinction between values and references is important from an implementation point of view: it would be wasteful to implement built-in types as references, so languages with a reference model generally implement built-in and user-defined types differently. Java reflects this distinction in the language semantics, calling for a value model of built-in types and a reference model for objects of user-defined class types.
递归类型通常用于创建链接数据结构。在大多数情况下,这些结构必须从堆中分配。在某些语言中,程序员负责释放不再需要的堆对象。在其他语言中,语言运行时系统会自动识别和回收此类垃圾。显式释放对程序员来说是一种负担,并且会导致内存泄漏和悬空引用的问题。虽然语言实现几乎从不尝试捕获内存泄漏(但是,请参阅探索 3.34和练习 C-8.28 ,了解有关此主题的一些想法),但有时会使用墓碑或锁和钥匙来捕获悬空引用。自动垃圾收集可能很昂贵,但事实证明这种技术越来越受欢迎。大多数垃圾收集技术要么依赖于引用计数,要么依赖于对当前可访问结构的某种形式的递归探索(跟踪)。后一类技术包括标记和清除、停止和复制以及分代收集。
Recursive types are generally used to create linked data structures. In most cases these structures must be allocated from a heap. In some languages, the programmer is responsible for deallocating heap objects that are no longer needed. In other languages, the language run-time system identifies and reclaims such garbage automatically. Explicit deallocation is a burden on the programmer, and leads to the problems of memory leaks and dangling references. While language implementations almost never attempt to catch memory leaks (see Exploration 3.34 and Exercise C-8.28, however, for some ideas on this subject) tombstones or locks and keys are sometimes used to catch dangling references. Automatic garbage collection can be expensive, but has proved increasingly popular. Most garbage-collection techniques rely either on reference counts or on some form of recursive exploration (tracing) of currently accessible structures. Techniques in this latter category include mark-and-sweep, stop-and-copy, and generational collection.
语言设计中很少有领域像 I/O 那样表现出如此多的变化。我们的讨论(主要在配套网站上)区分了交互式 I/O(它往往非常特定于平台)和基于文件的 I/O(它细分为临时文件,用于单个程序运行中的大量数据)和持久文件,用于离线存储。文件还细分为以模拟内存布局的二进制形式表示其信息的文件和与基于字符的文本相互转换的文件。与二进制文件相比,文本文件通常会产生时间和空间开销,但它们具有可移植性和人类可读性的重要优势。
Few areas of language design display as much variation as I/O. Our discussion (largely on the companion site) distinguished between interactive I/O, which tends to be very platform specific, and file-based I/O, which subdivides into temporary files, used for voluminous data within a single program run, and persistent files, used for off-line storage. Files also subdivide into those that represent their information in a binary form that mimics layout in memory and those that convert to and from character-based text. In comparison to binary files, text files generally incur both time and space overhead, but they have the important advantages of portability and human readability.
在对类型的考察中,我们看到了许多语言创新的例子,这些创新有助于提高程序的清晰度和可维护性,而且通常几乎没有性能开销。这些例子包括用户定义类型的原始想法(Algol 68)、枚举和子范围类型(Pascal)、记录和变体的集成(Pascal)以及 Ada 中子类型和派生类型之间的区别。在第 10 章中,我们将研究许多人认为过去 30 年最重要的语言创新,即面向对象。
In our examination of types, we saw many examples of language innovations that have served to improve the clarity and maintainability of programs, often with little or no performance overhead. Examples include the original idea of user-defined types (Algol 68), enumeration and subrange types (Pascal), the integration of records and variants (Pascal), and the distinction between subtypes and derived types in Ada. In Chapter 10 we will examine what many consider the most important language innovation of the past 30 years, namely object orientation.
与前面几章一样,我们看到了一些为了简化编译器或使编译后的程序更小或更快而牺牲语言的便利性、正交性或类型安全性的例子。例如,许多语言缺乏对记录的相等性测试,Pascal 和 Ada 要求记录的变体部分位于末尾,许多语言对集合的最大大小有限制,C 语言缺乏对 I/O 的类型检查,以及许多语言实现中普遍缺乏动态语义检查。我们还看到了一些语言特性的例子,这些特性至少部分是为了提高实现效率而引入的。这些特性包括打包类型、多长度数字类型、十进制算法和 C 样式指针算法。
As in previous chapters, we saw several cases in which a language's convenience, orthogonality, or type safety appears to have been compromised in order to simplify the compiler, or to make compiled programs smaller or faster. Examples include the lack of an equality test for records in many languages, the requirement in Pascal and Ada that the variant portion of a record lie at the end, the limitations in many languages on the maximum size of sets, the lack of type checking for I/O in C, and the general lack of dynamic semantic checks in many language implementations. We also saw several examples of language features introduced at least in part for the sake of efficient implementation. These include packed types, multilength numeric types, decimal arithmetic, and C-style pointer arithmetic.
同时,我们发现语言设计者和用户越来越愿意容忍语言实现的复杂性和成本,以改善语义。这里的例子包括 Ada 的类型安全变体记录;Java 和 C# 的标准长度数字类型;现代脚本语言的可变长度字符串和字符串运算符;Ada、C 和各种脚本语言中数组边界的后期绑定;以及 Fortran 90 中丰富的全数组和基于切片的数组操作。可能还包括 ML 及其后代的多态类型推断。当然,还应该包括自动垃圾收集的广泛采用。一旦对于生产质量命令式语言来说,垃圾收集被认为成本过高,现在它不仅是函数式和脚本语言的标准,而且是 Ada、Java、C#、Scala 和 Go 等语言的标准。
At the same time, one can identify a growing willingness on the part of language designers and users to tolerate complexity and cost in language implementation in order to improve semantics. Examples here include the type-safe variant records of Ada; the standard-length numeric types of Java and C#; the variable-length strings and string operators of modern scripting languages; the late binding of array bounds in Ada, C, and the various scripting languages; and the wealth of whole-array and slice-based array operations in Fortran 90. One might also include the polymorphic type inference of ML and its descendants. Certainly one should include the widespread adoption of automatic garbage collection. Once considered too expensive for production-quality imperative languages, garbage collection is now standard not only in functional and scripting languages, but in Ada, Java, C#, Scala, and Go, among others.
8.1 假设我们正在为一台具有 1 字节字符、2 字节短整型、4 字节整数和 8 字节实数的机器进行编译,并且其对齐规则要求每个原始数据元素的地址都是元素大小的偶数倍。进一步假设编译器不允许对字段进行重新排序。以下数组将占用多少空间?解释一下。A :记录数组 [0..9] s:短整型c:字符t:短整型d:字符r:实数i:整数
8.1 Suppose we are compiling for a machine with 1-byte characters, 2-byte shorts, 4-byte integers, and 8-byte reals, and with alignment rules that require the address of every primitive data element to be an even multiple of the element's size. Suppose further that the compiler is not permitted to reorder fields. How much space will be consumed by the following array? Explain.
A : array [0..9] of record
s : short
c : char
t : short
d : char
r : real
i : integer
8.2 在示例 8.10中,我们建议根据记录字段的对齐要求对其进行排序,以尽量减少空洞。在示例中,我们按最小对齐优先进行排序。如果我们按最长对齐优先进行排序,会发生什么情况?您认为这种方案有什么优点吗?有什么缺点吗?如果整个记录必须是最长对齐的偶数倍,这两种方法在所需的总空间上是否有差异?
8.2 In Example 8.10 we suggested the possibility of sorting record fields by their alignment requirement, to minimize holes. In the example, we sorted smallest-alignment-first. What would happen if we sorted longest-alignment-first? Do you see any advantages to this scheme? Any disadvantages? If the record as a whole must be an even multiple of the longest alignment, do the two approaches ever differ in total space required?
8.3 给出 Ada 代码,使用以下代码将小写字母映射到大写字母
8.3 Give Ada code to map from lowercase to uppercase letters, using
(b) 函数
(b) a function
注意语法的相似性:在两种情况下,upper('a')都是'A'。
Note the similarity of syntax: in both cases upper('a') is 'A'.
8.4 在8.2.2 节中,我们注意到,在具有动态数组和变量值模型的语言中,记录可能具有在编译时未知大小的字段。为了适应这种情况,我们建议对记录使用一个 dope 向量来跟踪字段的偏移量。
假设我们想为每个字段维护一个静态偏移量。我们能否设计出一种受图8.7的堆栈框架布局启发的替代策略,并将每个记录分为固定大小部分和可变大小部分?我们需要解决哪些问题?(提示:考虑嵌套记录。)
8.4 In Section 8.2.2 we noted that in a language with dynamic arrays and a value model of variables, records could have fields whose size is not known at compile time. To accommodate these, we suggested using a dope vector for the record, to track the offsets of the fields.
Suppose instead that we want to maintain a static offset for each field. Can we devise an alternative strategy inspired by the stack frame layout of Figure 8.7, and divide each record into a fixed-size part and a variable-size part? What problems would we need to address? (Hint: Consider nested records.)
8.5 解释如何扩展图 8.7以适应按值传递的子程序参数,但其形状直到运行时调用子程序时才知道。
8.5 Explain how to extend Figure 8.7 to accommodate subroutine arguments that are passed by value, but whose shape is not known until the subroutine is called at run time.
8.6 解释如何使用 C 中的指针获得 Fortran 90 的 allocate 语句对一维数组的效果。您可能会发现您的解决方案不能推广到多维数组。为什么?如果您熟悉 C++,请说明如何使用其类功能来解决问题。
8.6 Explain how to obtain the effect of Fortran 90's allocate statement for one-dimensional arrays using pointers in C. You will probably find that your solution does not generalize to multidimensional arrays. Why not? If you are familiar with C++, show how to use its class facilities to solve the problem.
8.7 示例 8.24考虑了二维字符数组的布局,仅计算了用于字符和指针的空间。如果空间是静态分配的,作为编译时已知的日期或关键字的全局数组,则这是合适的。相反,假设空间在堆中分配,每个连续存储块有 4 或 8 个字节的开销。这会如何改变空间效率的权衡?
8.7 Example 8.24, which considered the layout of a two-dimensional array of characters, counted only the space devoted to characters and pointers. This is appropriate if the space is allocated statically, as a global array of days or keywords known at compile time. Supposed instead that space is allocated in the heap, with 4 or 8 bytes of overhead for each contiguous block of storage. How does this change the tradeoffs in space efficiency?
8.8考虑 示例 8.25中的数组索引计算。假设i、j和k已加载到寄存器中,并且A的元素是整数,在 32 位机器上连续分配在内存中。用侧栏 5.1 中的伪汇编符号显示将A[i, j, k]加载到寄存器的指令序列。您可以假设存在一种能够按 2 的小幂缩放的索引寻址模式。假设最终的内存加载是缓存命中,您的代码在现代处理器上可能需要多少个周期?
8.8 Consider the array indexing calculation of Example 8.25. Suppose that i, j, and k are already loaded into registers, and that A's elements are integers, allocated contiguously in memory on a 32-bit machine. Show, in the pseudo-assembly notation of Sidebar 5.1, the instruction sequence to load A[i, j, k] into a register. You may assume the existence of an indexed addressing mode capable of scaling by small powers of two. Assuming the final memory load is a cache hit, how many cycles is your code likely to require on a modern processor?
8.9 继续上一个练习,假设A具有行指针布局,并且i、j和k再次在寄存器中可用。显示将A[i, j, k]加载到寄存器的伪汇编代码。假设所有内存加载都是缓存命中,您的代码在现代处理器上可能需要多少个周期?
8.9 Continuing the previous exercise, suppose that A has row-pointer layout, and that i, j, and k are again available in registers. Show pseudo-assembler code to load A[i, j, k] into a register. Assuming that all memory loads are cache hits, how many cycles is your code likely to require on a modern processor?
8.10 重复前两个练习,修改代码以包含数组下标边界的运行时检查。
8.10 Repeat the preceding two exercises, modifying your code to include runtime checking of array subscript bounds.
8.11 在8.2.3 节中,我们讨论了如何区分数组引用的常量部分和变量部分,以便高效地访问数组和记录对象的子部分。另一种方法是生成简单代码,并依靠编译器的代码改进器来查找常量部分、将它们组合在一起并在编译时计算它们。讨论每种方法的优缺点。
8.11 In Section 8.2.3 we discussed how to differentiate between the constant and variable portions of an array reference, in order to efficiently access the subparts of array and record objects. An alternative approach is to generate naive code and count on the compiler's code improver to find the constant portions, group them together, and calculate them at compile time. Discuss the advantages and disadvantages of each approach.
8.12 考虑以下在 64 位 x86 机器上编译的 C 声明:struct { int n; char c; } A[10][10];如果A[0][0]的地址是 1000(十进制),那么 A[3][7]的地址是多少?
8.12 Consider the following C declaration, compiled on a 64-bit x86 machine:
struct {
int n;
char c;
} A[10][10];
If the address of A[0][0] is 1000 (decimal), what is the address of A[3][7]?
8.13 假设我们在一台机器上为一种命令式语言生成代码,该机器有 8 字节浮点数、4 字节整数、1 字节字符,并且整数和浮点数都采用 4 字节对齐。假设此外,我们计划对多维数组使用连续的行主布局,我们不希望重新排序记录的字段或打包记录或数组,并且我们将假设所有数组下标都在范围内而不检查。
8.13 Suppose we are generating code for an imperative language on a machine with 8-byte floating-point numbers, 4-byte integers, 1-byte characters, and 4-byte alignment for both integers and floating-point numbers. Suppose further that we plan to use contiguous row-major layout for multidimensional arrays, that we do not wish to reorder fields of records or pack either records or arrays, and that we will assume without checking that all array subscripts are in bounds.
(a) 考虑以下变量声明:A : 实数数组 [1..10, 10..100] i : 整数x : 实数显示我们的编译器应为以下赋值生成的代码:x := A[3,i]。解释你是如何得出答案的。
(a) Consider the following variable declarations:
A : array [1..10, 10..100] of real
i : integer
x : real
Show the code that our compiler should generate for the following assignment: x := A[3,i]. Explain how you arrived at your answer.
(b) 考虑以下更复杂的声明:r :记录x :整数y :字符A :记录数组 [1..10, 10..20] z :实数B :字符数组 [0..71] j , k :整数假设这些声明是当前子程序的本地声明。注意A中索引的下限;第一个元素是A[1,10]。描述r在内存中的布局方式。然后显示将rA[2,j].B[k]加载到寄存器的代码。务必指出地址计算的哪些部分可以在编译时执行。
(b) Consider the following more complex declarations:
r : record
x : integer
y : char
A : array [1..10, 10..20] of record
z : real
B : array [0..71] of char
j, k : integer
Assume that these declarations are local to the current subroutine. Note the lower bounds on indices in A; the first element is A[1,10].
Describe how r would be laid out in memory. Then show code to load r.A[2,j].B[k] into a register. Be sure to indicate which portions of the address calculation could be performed at compile time.
8.14 假设A是一个 10×10 的(4 字节)整数数组,索引从[0][0]到[9][9] 。进一步假设A的地址当前在寄存器r1中,整数i的值当前在寄存器r2中,整数j的值当前在寄存器r3中。给出一个代码序列的伪汇编语言,该代码序列将A[i][j]
的值加载到寄存器r1中(a)假设A是使用(行主)连续分配实现的;(b)假设A是使用行指针实现的。伪代码的每一行都应对应于典型的现代机器上的一条指令。您可以根据需要使用任意数量的寄存器。您不需要保留r1、r2和r3中的值。您可以假设i和j在界限内,并且地址长度为 4 个字节。
哪个代码序列可能更快?为什么?
8.14 Suppose A is a 10×10 array of (4-byte) integers, indexed from [0][0] through [9][9]. Suppose further that the address of A is currently in register r1, the value of integer i is currently in register r2, and the value of integer j is currently in register r3.
Give pseudo-assembly language for a code sequence that will load the value of A[i][j] into register r1 (a) assuming that A is implemented using (row-major) contiguous allocation; (b) assuming that A is implemented using row pointers. Each line of your pseudocode should correspond to a single instruction on a typical modern machine. You may use as many registers as you need. You need not preserve the values in r1, r2, and r3. You may assume that i and j are in bounds, and that addresses are 4 bytes long.
Which code sequence is likely to be faster? Why?
8.15 指针和递归类型定义使确定类型结构等价性的算法变得复杂。例如,考虑以下定义:类型 A = 记录x : 指向 B 的指针y : 实数类型 B = 记录x : 指向 A 的指针y : 实数
7.2.1 节中给出的结构等价的简单定义(递归扩展子部分,直到只剩一串内置类型和类型构造函数;然后比较它们)不起作用:我们得到一个无限扩展(类型 A = 记录 x:指向记录的指针 x:指向记录的指针 x:指向记录的指针...)。显而易见的重新解释是说两个类型A和B是等价的,如果任何字段选择、数组下标、指针取消引用和其他操作序列将一个人带入A的结构并以内置类型结束,总是遇到相同的字段名称,并且在用于深入B的结构时以相同的内置类型结束- 反之亦然。根据这种重新解释,上面的A和B具有相同的类型。给出一个基于这种重新解释的算法,该算法可用于编译器中以确定结构等价。 (提示:最快的方法归功于 J. Král [ Krá73 ]。它基于用于查找接受给定正则语言的最小确定性有限自动机的算法。该算法在示例 2.15中概述;详细信息可在任何自动机理论教科书中找到[例如,[ HMU07 ]]。)
8.15 Pointers and recursive type definitions complicate the algorithm for determining structural equivalence of types. Consider, for example, the following definitions:
type A = record
x : pointer to B
y : real
type B = record
x : pointer to A
y : real
The simple definition of structural equivalence given in Section 7.2.1 (expand the subparts recursively until all you have is a string of built-in types and type constructors; then compare them) does not work: we get an infinite expansion (type A = record x : pointer to record x : pointer to record x : pointer to record …). The obvious reinterpretation is to say two types A and B are equivalent if any sequence of field selections, array subscripts, pointer dereferences, and other operations that takes one down into the structure of A, and that ends at a built-in type, always encounters the same field names, and ends at the same built-in type when used to dive into the structure of B—and vice versa. Under this reinterpretation, A and B above have the same type. Give an algorithm based on this reinterpretation that could be used in a compiler to determine structural equivalence. (Hint: The fastest approach is due to J. Král [Krá73]. It is based on the algorithm used to find the smallest deterministic finite automaton that accepts a given regular language. This algorithm was outlined in Example 2.15; details can be found in any automata theory textbook [e.g., [HMU07]].)
8.16 解释以下 C 声明的含义:double *a[n]; double (*b)[n]; double (*c[n])(); double (*d())[n];
8.16 Explain the meaning of the following C declarations:
double *a[n];
double (*b)[n];
double (*c[n])();
double (*d())[n];
8.17 在 Ada 83 中,指针(访问变量)只能指向堆中的对象。Ada 95 允许一种新的指针,即访问所有类型,也指向其他对象,前提是这些对象已声明为别名:type int_ptr is access all Integer; foo : 别名 Integer; ip : int_ptr; … ip := foo'Access; ' Access属性大致相当于 C 的“地址” (&)运算符。如何实现访问所有类型和别名对象?您的实现如何与堆中对象的自动垃圾收集(假设它存在)交互?
8.17 In Ada 83, pointers (access variables) can point only to objects in the heap. Ada 95 allows a new kind of pointer, the access all type, to point to other objects as well, provided that those objects have been declared to be aliased:
type int_ptr is access all Integer;
foo : aliased Integer;
ip : int_ptr;
…
ip := foo'Access;
The 'Access attribute is roughly equivalent to C's “address of” (&) operator. How would you implement access all types and aliased objects? How would your implementation interact with automatic garbage collection (assuming it exists) for objects in the heap?
8.18如 第 8.5.2 节所述,Ada 95 禁止访问所有指针引用任何生存期短于指针类型的对象。此规则可以在编译时完全执行吗?为什么或为什么不?
8.18 As noted in Section 8.5.2, Ada 95 forbids an access all pointer from referring to any object whose lifetime is briefer than that of the pointer's type. Can this rule be enforced completely at compile time? Why or why not?
8.19 在第 8.5 节中关于指针的大部分讨论中,我们隐式地假设每个指向堆的指针都指向动态分配的存储块的开头。在某些语言中,包括 Algol 68 和 C,指针也可能指向堆中块内的数据。如果您尝试实现悬垂引用的动态语义检查,或者自动垃圾收集(精确或保守),那么这种“内部指针”的存在会让您的任务变得多么复杂?
8.19 In much of the discussion of pointers in Section 8.5, we assumed implicitly that every pointer into the heap points to the beginning of a dynamically allocated block of storage. In some languages, including Algol 68 and C, pointers may also point to data inside a block in the heap. If you were trying to implement dynamic semantic checks for dangling references or, alternatively, automatic garbage collection (precise or conservative), how would your task be complicated by the existence of such “internal pointers”?
8.20
8.20
(a) 偶尔会有人建议垃圾收集语言应该提供删除操作作为优化:通过明确删除永远不会再使用的对象,程序员可以省去垃圾收集器自动查找和回收这些对象的麻烦,从而提高性能。你觉得这个建议怎么样?解释一下。
(a) Occasionally one encounters the suggestion that a garbage-collected language should provide a delete operation as an optimization: by explicitly delete-ing objects that will never be used again, the programmer might save the garbage collector the trouble of finding and reclaiming those objects automatically, thereby improving performance. What do you think of this suggestion? Explain.
(b) 或者,可以允许程序员“保有”某个对象,这样它就永远不会成为回收的候选对象。这是一个好主意吗?
(b) Alternatively, one might allow the programmer to “tenure” an object, so that it will never be a candidate for reclamation. Is this a good idea?
8.21 在示例 8.52中,我们指出函数式语言可以安全地使用引用计数,因为没有赋值语句可以防止它们引入循环。这并不完全正确;像 Lisp letrec这样的构造也可用于产生循环,只要循环定义名称的使用隐藏在每个定义中的lambda表达式中:(define foo (lambda () (letrec ((a (lambda(f) (if f #\A b))) (b (lambda(f) (if f #\B c))) (c (lambda(f) (if f #\C a)))) a)))函数a、b和c中的每一个都包含对下一个函数的引用:
8.21 In Example 8.52 we noted that functional languages can safely use reference counts since the lack of an assignment statement prevents them from introducing circularity. This isn't strictly true; constructs like the Lisp letrec can also be used to make cycles, so long as uses of circularly defined names are hidden inside lambda expressions in each definition:
(define foo
(lambda ()
(letrec ((a (lambda(f) (if f #\A b)))
(b (lambda(f) (if f #\B c)))
(c (lambda(f) (if f #\C a))))
a)))
Each of the functions a, b, and c contains a reference to the next:
如何在不放弃引用计数的情况下解决这种循环问题?
How might you address this circularity without giving up on reference counts?
8.22 以下是 Haskell 中标准快速排序算法的框架:
quicksort [] = []
quicksort (a : l) = quicksort […] ++ [a] ++ quicksort […] ++
运算符表示列表连接(类似于ML 中的@)。:运算符相当于 ML 的::或 Lisp 的cons。说明如何将两个省略的表达式表示为列表推导。
8.22 Here is a skeleton for the standard quicksort algorithm in Haskell:
quicksort [] = []
quicksort (a : l) = quicksort […] ++ [a] ++ quicksort […]
The ++ operator denotes list concatenation (similar to @ in ML). The : operator is equivalent to ML's :: or Lisp's cons. Show how to express the two elided expressions as list comprehensions.
8.23–8.31 更深入。
8.23–8.31 In More Depth.
8.32 如果您可以使用提供可选动态语义检查的编译器,用于检查数组下标越界、记录变体使用不当和/或悬空或未初始化的指针,请试验这些检查的成本。它们会增加多少执行大量检查访问的程序的执行时间?试验不同级别的优化(代码改进),看看每种优化对检查开销有何影响。
8.32 If you have access to a compiler that provides optional dynamic semantic checks for out-of-bounds array subscripts, use of an inappropriate record variant, and/or dangling or uninitialized pointers, experiment with the cost of these checks. How much do they add to the execution time of programs that make a significant number of checked accesses? Experiment with different levels of optimization (code improvement) to see what effect each has on the overhead of checks.
8.33 编写一个库包,供语言实现用来管理从非常大的基类型(例如,整数)中提取的元素集。您应该支持成员资格测试、并集、交集和差集。您的包是否从堆中分配内存?如果是这样,假定使用您的包的编译器需要做什么来确保在不再需要时回收空间?
8.33 Write a library package that might be used by a language implementation to manage sets of elements drawn from a very large base type (e.g., integer). You should support membership tests, union, intersection, and difference. Does your package allocate memory from the heap? If so, what would a compiler that assumed the use of your package need to do to make sure that space was reclaimed when no longer needed?
8.34 了解 SETL [ SDDS86 ],这是一门基于集合的编程语言,由纽约大学的 Jack Schwartz 设计。列出作为内置集合操作提供的机制。将此列表与其他编程语言的集合功能进行比较。SETL 实现可能使用哪些数据结构来表示程序中的集合?
8.34 Learn about SETL [SDDS86], a programming language based on sets, designed by Jack Schwartz of New York University. List the mechanisms provided as built-in set operations. Compare this list with the set facilities of other programming languages. What data structure(s) might a SETL implementation use to represent sets in a program?
8.35 HotSpot Java 编译器和虚拟机实现了一整套垃圾收集器:传统的分代收集器、老生代压缩收集器、低暂停时间并行收集器、高吞吐量并行收集器、与主程序并行运行的“大多数并发”老生代收集器。详细了解这些算法。每种算法何时使用,为什么使用?
8.35 The HotSpot Java compiler and virtual machine implements an entire suite of garbage collectors: a traditional generational collector, a compacting collector for the old generation, a low pause-time parallel collector for the nursery, a high-throughput parallel collector for the old generation, and a “mostly concurrent” collector for the old generation that runs in parallel with the main program. Learn more about these algorithms. When is each used, and why?
8.36在 Ada 中实现您最喜欢的垃圾收集算法。或者,在 C++ 中实现 shared_ptr类的简化版本,该类的存储将被垃圾收集。您将需要使用模板(泛型),以便您的类可以针对任意指向的类型进行实例化。
8.36 Implement your favorite garbage collection algorithm in Ada. Alternatively, implement a simplified version of the shared_ptr class in C++, for which storage is garbage collected. You'll want to use templates (generics) so that your class can be instantiated for arbitrary pointed-to types.
8.37 用你最喜欢的语言实现来试验垃圾收集的成本。它使用哪种收集器?你能创建出它表现特别好或特别差的人工程序吗?
8.37 Experiment with the cost of garbage collection in your favorite language implementation. What kind of collector does it use? Can you create artificial programs for which it performs particularly well or poorly?
8.38 了解Java 中的弱引用。它们如何与垃圾回收交互?它们与 C++ 中的弱引用对象相比如何?描述它们可能有用的几种场景。
8.38 Learn about weak references in Java. How do they interact with garbage collection? How do they compare to weak_ptr objects in C++? Describe several scenarios in which they may be useful.
8.39–8.41 更深入。
8.39–8.41 In More Depth.
虽然数组是最古老的复合数据类型,但它们仍然是语言设计中的一个活跃主题。2014 年 SIGPLAN 数组编程库、语言和编译器国际研讨会论文集 [ Hen14 ] 中可以找到具有代表性的当代作品。所有标准编译器文本都讨论了数组和记录的实现问题。Chamberlain 和 Snyder 描述了 ZPL 编程语言对稀疏数组的支持 [ CS01 ]。
While arrays are the oldest composite data type, they remain an active subject of language design. Representative contemporary work can be found in the proceedings of the 2014 SIGPLAN International Workshop on Libraries, Languages, and Compilers for Array Programming [Hen14]. Implementation issues for arrays and records are discussed in all the standard compiler texts. Chamberlain and Snyder describe support for sparse arrays in the ZPL programming language [CS01].
墓碑由 Lomet [ Lom75 , Lom85 ] 撰写。锁和钥匙由 Fischer 和 LeBlanc [ FL80 ] 撰写。后者还讨论了如何检查 Pascal 中的各种其他动态语义错误,包括变体记录中出现的错误。
Tombstones are due to Lomet [Lom75, Lom85]. Locks and keys are due to Fischer and LeBlanc [FL80]. The latter also discuss how to check for various other dynamic semantic errors in Pascal, including those that arise with variant records.
垃圾收集仍然是一个非常活跃的研究课题。许多正在进行的研究工作都在 ISMM(年度国际内存管理研讨会)上发表(www.sigplan.org/Conferences/ISMM)。常量空间(指针反转)标记-清除垃圾收集器由 Schorr 和 Waite [ SW67 ] 提出。停止-复制收集器由 Fenichel 和 Yochelson [ FY69 ] 开发,其思想基于 Minsky 的思想。Deutsch 和 Bobrow [ DB76 ] 描述了一种避免“stop-the-world”现象的增量式垃圾收集器。Wilson 和 Johnstone [ WJ93 ] 描述了一种较新的增量式收集器。第 8.5.3 节末尾描述的保守式收集器由 Boehm 和 Weiser [ BW88 ]提出。Cohen [ Coh81 ] 综述了截至 1981 年的垃圾收集技术;Wilson [ Wil92b ] 和 Jones 和 Lins [ JL96 ] 则提供了稍微新近的观点。 Bacon 等人 [ BCR04 ] 认为引用计数和跟踪实际上是同一底层存储问题的双重视角。
Garbage collection remains a very active topic of research. Much of the ongoing work is reported at ISMM, the annual International Symposium on Memory Management (www.sigplan.org/Conferences/ISMM). Constant-space (pointer-reversing) mark-and-sweep garbage collection is due to Schorr and Waite [SW67]. Stop-and-copy collection was developed by Fenichel and Yochelson [FY69], based on ideas due to Minsky. Deutsch and Bobrow [DB76] describe an incremental garbage collector that avoids the “stop-the-world” phenomenon. Wilson and Johnstone [WJ93] describe a later incremental collector. The conservative collector described at the end of Section 8.5.3 is due to Boehm and Weiser [BW88]. Cohen [Coh81] surveys garbage-collection techniques as of 1981; Wilson [Wil92b] and Jones and Lins [JL96] provide somewhat more recent views. Bacon et al. [BCR04] argue that reference counting and tracing are really dual views of the same underlying storage problem.
在第 3 章的介绍中,我们定义了抽象 程序员可以将名称与可能复杂的程序片段关联起来,然后可以根据其目的或功能而不是其实现来思考该程序片段。我们有时会区分控制抽象和数据抽象,前者的主要目的是执行明确定义的操作,后者的主要目的是表示信息。1我们将在第 10 章中更详细地讨论数据抽象。
In the introduction to Chapter 3, we defined abstraction as a process by which the programmer can associate a name with a potentially complicated program fragment, which can then be thought of in terms of its purpose or function, rather than in terms of its implementation. We sometimes distinguish between control abstraction, in which the principal purpose of the abstraction is to perform a well-defined operation, and data abstraction, in which the principal purpose of the abstraction is to represent information.1 We will consider data abstraction in more detail in Chapter 10.
子程序是大多数编程语言中控制抽象的主要机制。子程序代表调用者执行操作,调用者等待子程序完成后再继续执行。大多数子程序都是参数化的:调用者传递影响子程序行为的参数,或为其提供操作数据。参数也称为实际参数。它们在调用发生时映射到子程序的形式参数。返回值的子程序通常称为函数。不返回值的子程序通常称为过程。静态类型语言通常要求为每个被调用的子程序进行声明,以便编译器可以验证每个调用是否传递了正确数量和类型的参数。
Subroutines are the principal mechanism for control abstraction in most programming languages. A subroutine performs its operation on behalf of a caller, who waits for the subroutine to finish before continuing execution. Most subroutines are parameterized: the caller passes arguments that influence the subroutine's behavior, or provide it with data on which to operate. Arguments are also called actual parameters. They are mapped to the subroutine's formal parameters at the time a call occurs. A subroutine that returns a value is usually called a function. A subroutine that does not return a value is usually called a procedure. Statically typed languages typically require a declaration for every called subroutine, so the compiler can verify, for example, that every call passes the right number and types of arguments.
如第 3.2.2 节所述,在大多数语言中,参数和局部变量所消耗的存储空间可以分配在堆栈上。因此,我们从第 9.1 节开始本章,回顾堆栈的布局。然后,我们在第 9.2 节介绍用于维护此布局的调用序列。在此过程中,我们重新讨论了使用静态链访问嵌套子例程中的非局部变量,并考虑(在配套网站上)一种称为display 的替代机制,该机制具有类似的用途。我们还考虑了子例程内联和闭包的表示。为了说明一些可能的实现替代方案,我们(再次在配套网站上)提供了 LLVM 编译器的案例研究ARM 指令集和32 位和 64 位 x86 的gcc编译器。我们还讨论了SPARC 指令集的寄存器窗口机制。
As noted in Section 3.2.2, the storage consumed by parameters and local variables can in most languages be allocated on a stack. We therefore begin this chapter, in Section 9.1, by reviewing the layout of the stack. We then turn in Section 9.2 to the calling sequences that serve to maintain this layout. In the process, we revisit the use ofstatic chains to access nonlocal variables in nested subroutines, and consider (on the companion site) an alternative mechanism, known as a display, that serves a similar purpose. We also consider subroutine inlining and the representation of closures. To illustrate some of the possible implementation alternatives, we present (again on the companion site) case studies of the LLVM compiler for the ARM instruction set and the gcc compiler for 32- and 64-bit x86. We also discuss the register window mechanism of the SPARC instruction set.
在第 9.3 节中,我们将更仔细地研究子程序参数。我们考虑参数传递模式,它决定了子程序可以对其形式参数应用的操作以及这些操作对相应实际参数的影响。我们还考虑了命名参数和默认参数、可变数量的参数以及函数返回机制。
In Section 9.3 we look more closely at subroutine parameters. We consider parameter-passing modes, which determine the operations that a subroutine can apply to its formal parameters and the effects of those operations on the corresponding actual parameters. We also consider named and default parameters, variable numbers of arguments, and function return mechanisms.
在第 9.4 节中,我们考虑了异常情况的处理。虽然异常有时可以局限于当前子程序,但一般情况下,它们需要一种机制来“弹出”嵌套上下文而不返回,以便可以在调用上下文中进行恢复。在第 9.5 节中,我们考虑了协程,它允许程序维护两个或多个执行上下文,并在它们之间来回切换。协程可用于实现迭代器(第 6.5.3 节),但它们还有其他用途,特别是在模拟和服务器程序中。在第13 章中,我们将使用它们作为并发(“准并行”)线程的基础。最后,在第 9.6 节中,我们考虑异步事件- 发生在程序之外但程序需要对其作出响应的事情。
In Section 9.4, we consider the handling of exceptional conditions. While exceptions can sometimes be confined to the current subroutine, in the general case they require a mechanism to “pop out of” a nested context without returning, so that recovery can occur in the calling context. In Section 9.5, we consider coroutines, which allow a program to maintain two or more execution contexts, and to switch back and forth among them. Coroutines can be used to implement iterators (Section 6.5.3), but they have other uses as well, particularly in simulation and in server programs. In Chapter 13 we will use them as the basis for concurrent (“quasiparallel”) threads. Finally, in Section 9.6 we consider asynchronous events—things that happen outside a program, but to which it needs to respond.
维护子程序调用堆栈是调用序列(调用者在子程序调用之前和之后立即执行的代码)以及子程序本身的序言(在开头执行的代码)和尾声(在结尾执行的代码)的责任。有时术语“调用序列”用于指代调用者、序言和尾声的组合操作。
Maintenance of the subroutine call stack is the responsibility of the calling sequence—the code executed by the caller immediately before and after a subroutine call—and of the prologue (code executed at the beginning) and epilogue (code executed at the end) of the subroutine itself. Sometimes the term “calling sequence” is used to refer to the combined operations of the caller, the prologue, and the epilogue.
在进入子程序时必须完成的任务包括传递参数、保存返回地址、更改程序计数器、更改堆栈指针以分配空间、保存包含可能被调用方覆盖但仍在调用方中有效(可能需要)的值的寄存器(包括帧指针)、更改帧指针以引用新帧以及为新帧中需要它的任何对象执行初始化代码。在退出时必须完成的任务包括传递返回参数或函数值、为需要它的任何本地对象执行终结代码、释放堆栈帧(恢复堆栈指针)、恢复其他已保存的寄存器(包括帧指针)以及恢复程序计数器。其中一些任务(例如传递参数)必须由调用方执行,因为它们在不同的调用中有所不同。但是,大多数任务都可以由调用方或被调用方执行。一般来说,如果被调用者做尽可能多的工作,我们就会节省空间:被调用者中执行的任务在目标程序中只出现一次,但调用者中执行的任务会出现在每个调用站点,并且典型的子程序在多个地方被调用。
Tasks that must be accomplished on the way into a subroutine include passing parameters, saving the return address, changing the program counter, changing the stack pointer to allocate space, saving registers (including the frame pointer) that contain values that may be overwritten by the callee but are still live (potentially needed) in the caller, changing the frame pointer to refer to the new frame, and executing initialization code for any objects in the new frame that require it. Tasks that must be accomplished on the way out include passing return parameters or function values, executing finalization code for any local objects that require it, deallocating the stack frame (restoring the stack pointer), restoring other saved registers (including the frame pointer), and restoring the program counter. Some of these tasks (e.g., passing parameters) must be performed by the caller, because they differ from call to call. Most of the tasks, however, can be performed either by the caller or the callee. In general, we will save space if the callee does as much work as possible: tasks performed in the callee appear only once in the target program, but tasks performed in the caller appear at every call site, and the typical subroutine is called in more than one place.
最棘手的分工问题可能与保存寄存器有关。理想的方法(参见 C-5.5.2 节)是精确保存那些在调用方中有效且在被调用方中用于其他目的的寄存器。然而,由于单独编译,很难(但并非不可能)确定这个相交集。一个更简单的解决方案是让调用方保存所有正在使用的寄存器,或者让被调用方保存它将覆盖的所有寄存器。
Perhaps the trickiest division-of-labor issue pertains to saving registers. The ideal approach (see Section C-5.5.2) is to save precisely those registers that are both live in the caller and needed for other purposes in the callee. Because of separate compilation, however, it is difficult (though not impossible) to determine this intersecting set. A simpler solution is for the caller to save all registers that are in use, or for the callee to save all registers that it will overwrite.
许多处理器(包括C-9.2.2 节案例研究中描述的 ARM 和 x86)的调用序列约定都达成了某种妥协:未保留用于特殊目的的寄存器被分成两组,大小大致相等。一组由调用者负责,另一组由被调用者负责。被调用者可以假设调用者保存集中的任何寄存器中都没有有价值的内容;调用者可以假设没有被调用者会破坏被调用者保存集中的任何寄存器的内容。为了节省代码大小,编译器尽可能使用被调用者保存的寄存器来保存局部变量和其他长期值。它使用调用者保存集来保存临时值,这些临时值不太可能在调用之间需要。这些约定的结果是,调用者保存寄存器很少被任何一方保存:被调用者知道它们是调用者的责任,并且调用者知道它们不包含任何重要内容。
Calling sequence conventions for many processors, including the ARM and x86 described in the case studies of Section C-9.2.2, strike something of a compromise: registers not reserved for special purposes are divided into two sets of approximately equal size. One set is the caller's responsibility, the other is the callee's responsibility. A callee can assume that there is nothing of value in any of the registers in the caller-saves set; a caller can assume that no callee will destroy the contents of any registers in the callee-saves set. In the interests of code size, the compiler uses the callee-saves registers for local variables and other long-lived values whenever possible. It uses the caller-saves set for transient values, which are less likely to be needed across calls. The result of these conventions is that the caller-saves registers are seldom saved by either party: the callee knows that they are the caller's responsibility, and the caller knows that they don't contain anything important.
在具有嵌套子例程的语言中,维护静态链所需的工作至少有一部分必须由调用者而不是被调用者执行,因为这项工作取决于调用者的词汇嵌套深度。标准方法是让调用者计算被调用者的静态链接并将其作为额外的隐藏参数传递。会出现两种子情况:
In languages with nested subroutines, at least part of the work required to maintain the static chain must be performed by the caller, rather than the callee, because this work depends on the lexical nesting depth of the caller. The standard approach is for the caller to compute the callee's static link and to pass it as an extra, hidden parameter. Two subcases arise:
1. 被调用者(直接)嵌套在调用者内部。在这种情况下,被调用者的静态链接应引用调用者的框架。因此,调用者将其自己的框架指针作为被调用者的静态链接传递。
1. The callee is nested (directly) inside the caller. In this case, the callee's static link should refer to the caller's frame. The caller therefore passes its own frame pointer as the callee's static link.
2. 被调用者的范围是“向外” k≥0——更接近词汇嵌套的外层。在这种情况下,围绕被调用者的所有范围也围绕调用者(否则被调用者将不可见)。调用者对其自己的静态链接进行k次解引用,并将结果作为被调用者的静态链接传递。
2. The callee is k ≥ 0 scopes “outward”—closer to the outer level of lexical nesting. In this case, all scopes that surround the callee also surround the caller (otherwise the callee would not be visible). The caller dereferences its own static link k times and passes the result as the callee's static link.
在常见情况下,调用序列、序言和结尾的许多部分都可以省略。如果硬件将返回地址传递到寄存器中,那么叶例程(在返回之前不进行其他调用的子例程)2可以简单地将其留在那里;它不需要将其保存在堆栈中。同样,它不需要保存静态链接或任何调用者保存的寄存器。
Many parts of the calling sequence, prologue, and epilogue can be omitted in common cases. If the hardware passes the return address in a register, then a leaf routine (a subroutine that makes no additional calls before returning)2 can simply leave it there; it does not need to save it in the stack. Likewise it need not save the static link or any caller-saves registers.
没有局部变量且无需保存或恢复的子程序甚至可能不需要 RISC 计算机上的堆栈框架。最简单的子程序(例如,用于计算标准数学函数的库程序)可能根本不接触内存,除非获取指令:它们可能在寄存器中获取参数,完全在(调用者保存)寄存器中计算,不调用其他程序,并在寄存器中返回结果。因此,它们可能非常快。
A subroutine with no local variables and nothing to save or restore may not even need a stack frame on a RISC machine. The simplest subroutines (e.g., library routines to compute the standard mathematical functions) may not touch memory at all, except to fetch instructions: they may take their arguments in registers, compute entirely in (caller-saves) registers, call no other routines, and return their results in registers. As a result they may be extremely fast.
静态链的一个缺点是,访问k级范围内的对象需要对静态链进行k次取消引用。如果本地对象可以通过一次(位移模式)内存访问加载到寄存器中,则k级对象将需要k + 1 次内存访问。通过使用显示,可以将这个数字减少为一个常数。
One disadvantage of static chains is that access to an object in a scope k levels out requires that the static chain be dereferenced k times. If a local object can be loaded into a register with a single (displacement mode) memory access, an object k levels out will require k + 1 memory accesses. This number can be reduced to a constant by use of a display.
更深入地
IN MORE DEPTH
如配套网站所述,显示是一个替代静态链的小数组。显示的第j个元素包含对词汇嵌套级别j上最近活动子例程的框架的引用。如果当前活动例程嵌套深度i > 3 级,则显示的元素i − 1、i − 2 和i − 3 包含静态链的前三个链接的值。可以在显示的元素j = i − k中存储的地址的静态已知偏移处找到第k级的对象。
As described on the companion site, a display is a small array that replaces the static chain. The jth element of the display contains a reference to the frame of the most recently active subroutine at lexical nesting level j. If the currently active routine is nested i > 3 levels deep, then elements i − 1, i − 2, and i − 3 of the display contain the values that would have been the first three links of the static chain. An object k levels out can be found at a statically known offset from the address stored in element j = i − k of the display.
对于大多数程序来说,在子程序调用序列中维护显示的成本往往略高于维护静态链的成本。同时,现代编译器降低了取消引用静态链的成本,这些编译器往往会在适当的时候很好地将链接缓存在寄存器中。这些观察结果,加上语言(尤其是从 C 派生出来的语言)中子程序不嵌套的趋势,使得今天的显示比 20 世纪 70 年代不那么常见了。
For most programs the cost of maintaining a display in the subroutine calling sequence tends to be slightly higher than that of maintaining a static chain. At the same time, the cost of dereferencing the static chain has been reduced by modern compilers, which tend to do a good job of caching the links in registers when appropriate. These observations, combined with the trend toward languages (those descended from C in particular) in which subroutines do not nest, have made displays less common today than they were in the 1970s.
调用序列在不同的机器之间,甚至在不同的编译器之间都有很大差异,尽管硬件供应商通常会发布针对各自架构的建议约定,以促进不同编译器生成的程序组件之间的互操作性。许多最显著的差异反映了随着时间的推移,寄存器的使用越来越频繁,内存的使用越来越少。这种演变反映了至少三个重要的技术趋势:寄存器集的大小不断增加,速度差距越来越大寄存器和内存(甚至 L1 缓存),以及编译器和处理器通过重新排序指令来提高性能的能力(至少当操作数都在寄存器中时)。
Calling sequences differ significantly from machine to machine and even compiler to compiler, though hardware vendors typically publish suggested conventions for their respective architectures, to promote interoperability among program components produced by different compilers. Many of the most significant differences reflect an evolution over time toward heavier use of registers and lighter use of memory. This evolution reflects at least three important technological trends: the increasing size of register sets, the increasing gap in speed between registers and memory (even L1 cache), and the increasing ability of both compilers and processors to improve performance by reordering instructions—at least when operands are all in registers.
较旧的编译器(尤其是寄存器数量较少的机器)倾向于在堆栈上传递参数;较新的编译器(尤其是寄存器组较大的机器)倾向于在寄存器中传递参数。较旧的体系结构倾向于提供将返回地址推送到堆栈的子例程调用指令;较新的体系结构倾向于将返回地址放在寄存器中。
Older compilers, particularly for machines with a small number of registers, tend to pass arguments on the stack; newer compilers, particularly for machines with larger register sets, tend to pass arguments in registers. Older architectures tend to provide a subroutine call instruction that pushes the return address onto the stack; newer architectures tend to put the return address in a register.
许多机器提供在子程序调用序列中使用的特殊指令。例如,在 x86 上,enter和leave指令通过同时更新帧指针和堆栈指针来分配和释放堆栈帧。在 ARM 上,stm(存储多个)和ldm(加载多个)指令保存和恢复任意寄存器组;在一个常见的习惯用法中,保存的集合包括返回地址(“链接寄存器”);当恢复的集合包括程序计数器(在同一位置)时,ldm可以在一条指令中弹出一组寄存器并从子程序返回。
Many machines provide special instructions of use in the subroutine-call sequence. On the x86, for example, enter and leave instructions allocate and deallocate stack frames, via simultaneous update of the frame pointer and stack pointer. On the ARM, stm (store multiple) and ldm (load multiple) instructions save and restore arbitrary groups of registers; in one common idiom, the saved set includes the return address (“link register”); when the restored set includes the program counter (in the same position), ldm can pop a set of registers and return from the subroutine in a single instruction.
还有一种趋势(虽然不太一致)是不再使用专用的帧指针寄存器。在较旧的编译器中,对于较旧的机器,通常使用push和pop指令来传递基于堆栈的参数。由此导致的 sp 值的不稳定性使得很难(但并非不可能)使用该寄存器作为访问局部变量的基础。单独的帧指针简化了代码生成和符号调试。同时,它在子例程调用序列中引入了额外的指令,并将可用于其他目的的寄存器数量减少了一个。现代编译器编写者越来越愿意牺牲复杂性来换取性能,并且经常放弃使用帧指针,至少在简单的例程中是这样。
There has also been a trend—though a less consistent one—away from the use of a dedicated frame pointer register. In older compilers, for older machines, it was common to use push and pop instructions to pass stack-based arguments. The resulting instability in the value of the sp made it difficult (though not impossible) to use that register as the base for access to local variables. A separate frame pointer simplified both code generation and symbolic debugging. At the same time, it introduced additional instructions into the subroutine calling sequence, and reduced by one the number of registers available for other purposes. Modern compiler writers are increasingly willing to trade complexity for performance, and often dispense with the frame pointer, at least in simple routines.
更深入地
IN MORE DEPTH
在配套站点上,我们详细介绍了具有代表性的一对编译器的堆栈布局约定和调用序列:用于 32 位 ARMv7 架构的 LLVM 编译器,以及用于 32 位和 64 位 x86的 gcc编译器。LLVM 是一种中/后端组合,最初由伊利诺伊大学开发,现在广泛应用于学术界和工业界。除其他外,它构成了 iPhone(iOS)和 Android 设备标准工具链的骨干。GNU 编译器集合gcc是开源运动的基石,被广泛应用于各种各样的笔记本电脑、台式机和服务器。LLVM 和gcc都具有适用于许多目标架构的后端和适用于许多编程语言的前端。我们专注于它们对 C 的支持,从某种意义上说,C 的约定是其他语言的“最低公分母”。
On the companion site we look in some detail at the stack layout conventions and calling sequences of a representative pair of compilers: the LLVM compiler for the 32-bit ARMv7 architecture, and the gcc compiler for the 32- and 64-bit x86. LLVM is a middle/back end combination originally developed at the University of Illinois and now used extensively in both academia and industry. Among other things, it forms the backbone of the standard tool chains for both iPhone (iOS) and Android devices. The GNU compiler collection, gcc, is a cornerstone of the open source movement, used across a huge variety of laptops, desktops, and servers. Both LLVM and gcc have back ends for many target architectures, and front ends for many programming languages. We focus on their support for C, whose conventions are in some sense a “lowest common denominator” for other languages.
作为在子程序调用和返回时保存和恢复寄存器的替代方法,最初的 Berkeley RISC 计算机 [ PD80、Pat85 ] 引入了一种称为寄存器窗口的硬件机制。基本思想是将 ISA 有限的寄存器名称集映射到更大的物理寄存器集合的某个子集(窗口)上,并在进行子程序调用时更改映射。旧映射和新映射略有重叠,允许在交集处传递参数(并返回函数结果)。
As an alternative to saving and restoring registers on subroutine calls and returns, the original Berkeley RISC machines [PD80, Pat85] introduced a hardware mechanism known as register windows. The basic idea is to map the ISA's limited set of register names onto some subset (window) of a much larger collection of physical registers, and to change the mapping when making subroutine calls. Old and new mappings overlap a bit, allowing arguments to be passed (and function results returned) in the intersection.
更深入地
IN MORE DEPTH
我们在配套网站上更详细地讨论了寄存器窗口。它们已出现在多种商用处理器中,最著名的是 Sun SPARC 和 Intel IA-64 (Itanium)。
We consider register windows in more detail on the companion site. They have appeared in several commercial processors, most notably the Sun SPARC and the Intel IA-64 (Itanium).
作为基于堆栈的调用约定的替代方案,许多语言实现允许在调用点内联扩展某些子例程。“被调用”例程的副本成为“调用者”的一部分;不会发生实际的子例程调用。内联扩展可避免各种开销,包括空间分配、调用和返回的分支延迟、维护静态链或显示以及(通常)保存和恢复寄存器。它还允许编译器执行代码改进,例如全局寄存器分配、指令调度和跨子例程边界的公共子表达式消除 - 这是大多数编译器无法做到的。
As an alternative to stack-based calling conventions, many language implementations allow certain subroutines to be expanded in-line at the point of call. A copy of the “called” routine becomes a part of the “caller”; no actual subroutine call occurs. In-line expansion avoids a variety of overheads, including space allocation, branch delays from the call and return, maintaining the static chain or display, and (often) saving and restoring registers. It also allows the compiler to perform code improvements such as global register allocation, instruction scheduling, and common subexpression elimination across the boundaries between subroutines—something that most compilers can't do otherwise.
大多数子程序都是参数化的:它们接受控制其行为某些方面或指定它们要操作的数据的参数。出现在子程序声明中的参数名称称为形式参数。在特定调用中传递给子程序的变量和表达式称为实际参数。我们一直将实际参数称为参数。在以下两小节中,我们将讨论最常见的参数传递模式,其中大多数是通过传递值、引用或闭包来实现的。在9.3.3 节中,我们将介绍其他机制,包括默认(可选)参数、命名参数和可变长度参数列表。最后,在9.3.4 节中,我们将考虑从函数返回值的机制。
Most subroutines are parameterized: they take arguments that control certain aspects of their behavior, or specify the data on which they are to operate. Parameter names that appear in the declaration of a subroutine are known as formal parameters. Variables and expressions that are passed to a subroutine in a particular call are known as actual parameters. We have been referring to actual parameters as arguments. In the following two subsections, we discuss the most common parameter-passing modes, most of which are implemented by passing values, references, or closures. In Section 9.3.3 we will look at additional mechanisms, including default (optional) parameters, named parameters, and variable-length argument lists. Finally, in Section 9.3.4 we will consider mechanisms for returning values from functions.
到目前为止,在对子程序的讨论中,我们忽略了控制参数传递以及确定实际参数和形式参数之间关系的语义规则。一些语言(包括 C、Fortran、ML 和 Lisp)定义了一组适用于所有参数的规则。其他语言(包括 Ada、C++ 和 Swift)提供了两组或更多组规则,分别对应于不同的参数传递模式。与语言设计的许多方面一样,语义细节在很大程度上受到实现问题的影响。
In our discussion of subroutines so far, we have glossed over the semantic rules that govern parameter passing, and that determine the relationship between actual and formal parameters. Some languages, including C, Fortran, ML, and Lisp, define a single set of rules, which apply to all parameters. Other languages, including Ada, C++, and Swift, provide two or more sets of rules, corresponding to different parameter-passing modes. As in many aspects of language design, the semantic details are heavily influenced by implementation issues.
对于值参数,在调用子程序时,每个实际参数都会被赋值给相应的形式参数;从此以后,两者是独立的。对于引用参数,每个形式参数都会在子程序主体中为相应的实际参数引入一个新名称。如果实际参数在子程序中以其原始名称可见(如果它在周围范围内声明,通常就是这种情况),则这两个名称是同一对象的别名,通过一个名称所做的更改将通过另一个名称可见。在大多数语言中(Fortran 是个例外;见下文),要通过引用传递的实际参数必须是左值;它不能是算术运算的结果,也不能是任何其他没有地址的值。
With value parameters, each actual parameter is assigned into the corresponding formal parameter when a subroutine is called; from then on, the two are independent. With reference parameters, each formal parameter introduces, within the body of the subroutine, a new name for the corresponding actual parameter. If the actual parameter is also visible within the subroutine under its original name (as will generally be the case if it is declared in a surrounding scope), then the two names are aliases for the same object, and changes made through one will be visible through the other. In most languages (Fortran is an exception; see below) an actual parameter that is to be passed by reference must be an l-value; it cannot be the result of an arithmetic operation, or any other value without an address.
Fortran 通过引用传递所有参数,但不要求每个实际参数都是左值。如果内置表达式出现在参数列表中,则编译器会创建一个临时变量来保存该值,并通过引用传递此变量。如果 Fortran 子例程需要修改其形式参数的值而不修改其实际参数,则必须将这些值复制到局部变量中,然后修改这些变量。
Fortran passes all parameters by reference, but does not require that every actual parameter be an l-value. If a built-up expression appears in an argument list, the compiler creates a temporary variable to hold the value, and passes this variable by reference. A Fortran subroutine that needs to modify the values of its formal parameters without modifying its actual parameters must copy the values into local variables, and modify those instead.
按值调用和按引用调用在具有变量值模型的语言中最有意义:它们决定我们是复制变量还是传递它的别名。在 Smalltalk、Lisp、ML 或 Ruby 等语言中,这两个选项都没有意义,因为在这些语言中变量已经是一个引用。在这里,最自然的方法是简单地传递引用本身,并让实际参数和形式参数引用同一个对象。Clu 将这种模式称为共享调用。它不同于按值调用,因为虽然我们确实将实际参数复制到形式参数中,但它们都是引用;如果我们修改形式参数引用的对象,则在子例程返回后,程序将能够通过实际参数看到这些更改。共享调用也不同于按引用调用,因为虽然被调用的例程可以更改实际参数引用的对象的值,但它不能让参数引用不同的对象。
Call by value and call by reference make the most sense in a language with a value model of variables: they determine whether we copy the variable or pass an alias for it. Neither option really makes sense in a language like Smalltalk, Lisp, ML, or Ruby, in which a variable is already a reference. Here it is most natural simply to pass the reference itself, and let the actual and formal parameters refer to the same object. Clu called this mode call by sharing. It is different from call by value because, although we do copy the actual parameter into the formal parameter, both of them are references; if we modify the object to which the formal parameter refers, the program will be able to see those changes through the actual parameter after the subroutine returns. Call by sharing is also different from call by reference, because although the called routine can change the value of the object to which the actual parameter refers, it cannot make the argument refer to a different object.
正如我们在6.1.2和8.5.1节中提到的,变量的引用模型不一定要求每个对象都通过地址间接访问:实现可以创建不可变对象(数字、字符等)的多个副本并直接访问它们。因此,对于不可变类型的小对象,共享调用通常与值调用相同。
As we noted in Sections 6.1.2 and 8.5.1, a reference model of variables does not necessarily require that every object be accessed indirectly by address: the implementation can create multiple copies of immutable objects (numbers, characters, etc.) and access them directly. Call by sharing is thus commonly implemented the same as call by value for small objects of immutable type.
为了保持变量的混合模型,Java 对原始内置类型(所有类型都是值)的变量使用按值调用,对用户定义类类型(所有类型都是引用)的变量使用共享调用。一个有趣的结果是,Java 子例程无法更改原始类型的实际参数的值。C# 中默认采用类似的方法,但由于该语言允许用户创建值(结构)和引用(类)类型,因此这两种情况都被视为按值调用。也就是说,无论变量是值还是引用,我们总是通过复制来传递它。(有些作者以同样的方式描述 Java。)
In keeping with its hybrid model of variables, Java uses call by value for variables of primitive, built-in types (all of which are values), and call by sharing for variables of user-defined class types (all of which are references). An interesting consequence is that a Java subroutine cannot change the value of an actual parameter of primitive type. A similar approach is the default in C#, but because the language allows users to create both value (struct) and reference (class) types, both cases are considered call by value. That is, whether a variable is a value or a reference, we always pass it by copying. (Some authors describe Java the same way.)
如果需要,C# 中的参数可以通过引用传递,方法是用ref或out关键字标记形式参数和每个相应的参数。 这两种模式都是通过传递地址来实现的;它们的不同之处在于,ref参数必须在调用之前明确赋值,如第 6.1.3 节所述;而out参数则不需要。 因此,与 Java 相反,如果参数通过ref或out传递,C# 子程序可以更改原始类型的实际参数的值。 类似地,如果类(引用)类型的变量作为ref或out参数传递,它最终可能会在子程序执行后引用不同的对象 —— 这是通过共享调用无法实现的。
When desired, parameters in C# can be passed by reference instead, by labeling both a formal parameter and each corresponding argument with the ref or out keyword. Both of these modes are implemented by passing an address; they differ in that a ref argument must be definitely assigned prior to the call, as described in Section 6.1.3; an out argument need not. In contrast to Java, therefore, a C# subroutine can change the value of an actual parameter of primitive type, if the parameter is passed ref or out. Similarly, if a variable of class (reference) type is passed as a ref or out parameter, it may end up referring to a different object as a result of subroutine execution—something that is not possible with call by sharing.
从历史上看,在同时提供值参数和引用参数的语言(例如 Pascal 或 Modula)中,程序员在选择值参数和引用参数时可能会考虑两个主要问题。首先,如果被调用的例程应该更改实际参数(参数)的值,那么程序员必须通过引用传递该参数。相反,为了确保被调用的例程不能修改参数,程序员可以通过值传递参数。其次,值参数的实现会将实际值复制到形式值,当参数很大时,这可能是一个耗时的操作。引用参数可以通过传递地址来实现。(当然,访问通过引用传递的参数需要额外的间接层。如果参数使用得足够频繁,这种间接的成本可能超过复制参数的成本。)
Historically, there were two principal issues that a programmer might consider when choosing between value and reference parameters in a language (e.g., Pascal or Modula) that provided both. First, if the called routine was supposed to change the value of an actual parameter (argument), then the programmer had to pass the parameter by reference. Conversely, to ensure that the called routine could not modify the argument, the programmer could pass the parameter by value. Second, the implementation of value parameters would copy actuals to formals, a potentially time-consuming operation when arguments were large. Reference parameters can be implemented simply by passing an address. (Of course, accessing a parameter that is passed by reference requires an extra level of indirection. If the parameter were used often enough, the cost of this indirection might outweigh the cost of copying the argument.)
大值参数的潜在低效率可能会促使程序员通过引用传递参数,而从语义上讲,通过值传递更为合适。例如,Pascal 程序员通常被教导对需要修改的参数和非常大的参数使用var(引用)参数。类似地,当今的 C 程序员通常被教导对需要修改的参数和非常大的参数传递指针(用&创建)。不幸的是,后一种理由往往会导致有缺陷的代码,其中子例程修改了调用者本想保持不变的参数。
The potential inefficiency of large value parameters may prompt programmers to pass an argument by reference when passing by value would be semantically more appropriate. Pascal programmers, for example, were commonly taught to use var (reference) parameters both for arguments that need to be modified and for arguments that are very large. In a similar vein, C programmers today are commonly taught to pass pointers (created with &) for both to-be-modified and very large arguments. Unfortunately, the latter justification tends to lead to buggy code, in which a subroutine modifies an argument that the caller meant to leave unchanged.
为了将引用参数的效率与值参数的安全性结合起来,Modula-3 提供了一种READONLY参数模式。任何以READONLY开头的形式参数都不能被调用的例程更改:编译器阻止程序员在任何赋值语句的左侧使用该形式参数、从文件中读取它或通过引用将其传递给任何其他子例程。小的READONLY参数通常通过传递值来实现;较大的READONLY参数通过传递地址来实现。与 Fortran 一样,Modula-3 编译器将创建一个临时变量来保存作为大型READONLY参数传递的任何构建表达式的值。
To combine the efficiency of reference parameters and the safety of value parameters, Modula-3 provided a READONLY parameter mode. Any formal parameter whose declaration was preceded by READONLY could not be changed by the called routine: the compiler prevented the programmer from using that formal parameter on the left-hand side of any assignment statement, reading it from a file, or passing it by reference to any other subroutine. Small READONLY parameters were generally implemented by passing a value; larger READONLY parameters were implemented by passing an address. As in Fortran, a Modula-3 compiler would create a temporary variable to hold the value of any built-up expression passed as a large READONLY parameter.
参数模式(尤其是READONLY模式)的一个传统问题是,它们容易混淆关键的实际问题(实现传递的是值还是引用?)和两个语义问题:被调用者是否被允许更改形式参数,如果可以,更改是否会反映在实际参数中?C 通过强制程序员使用指针显式传递引用,将实用问题分开。然而,它的const模式有双重作用: const foo* p的目的是为了保护实际参数不被更改,还是为了记录子例程将形式参数视为常量而不是变量的事实,还是两者兼而有之?
One traditional problem with parameter modes—and with the READONLY mode in particular—is that they tend to confuse the key pragmatic issue (does the implementation pass a value or a reference?) with two semantic issues: is the callee allowed to change the formal parameter and, if so, will the changes be reflected in the actual parameter? C keeps the pragmatic issue separate, by forcing the programmer to pass references explicitly with pointers. Still, its const mode serves double duty: is the intent of const foo* p to protect the actual parameter from change, or to document the fact that the subroutine thinks of the formal parameter as a constant rather than a variable, or both?
Ada 提供三种参数传递模式,分别称为in、out和in out。输入参数将信息从调用者传递给被调用者;它们可以被调用者读取但不能写入。输出参数将信息从被调用者传递给调用者。在 Ada 83 中,它们可以被调用者写入但不能读取;在 Ada 95 中,它们既可以读取也可以写入,但它们的生命周期开始时未初始化。输入输出参数双向传递信息;它们既可以读取也可以写入。对输出或输入输出参数的更改始终会更改实际参数。
Ada provides three parameter-passing modes, called in, out, and in out. In parameters pass information from the caller to the callee; they can be read by the callee but not written. Out parameters pass information from the callee to the caller. In Ada 83 they can be written by the callee but not read; in Ada 95 they can be both read and written, but they begin their life uninitialized. In out parameters pass information in both directions; they can be both read and written. Changes to out or in out parameters always change the actual parameter.
对于标量和访问(指针)类型的参数,Ada 规定所有三种模式都应通过复制值来实现。因此,对于这些参数,in是按值调用,in out是按值/结果调用,out只是按结果调用(子程序返回时,形式参数的值被复制到实际参数中)。但是,对于大多数构造类型的参数,Ada 明确允许实现传递值或引用。在大多数语言中,这两种不同的机制会导致不同的语义:对按引用传递的in out参数所做的更改将立即影响实际参数;对按值传递的in out参数所做的更改直到子程序返回才会影响实际参数。如示例 9.12中所述,在存在别名的情况下,这种差异可能导致不同的行为。
For parameters of scalar and access (pointer) types, Ada specifies that all three modes are to be implemented by copying values. For these parameters, then, in is call by value, in out is call by value/result, and out is simply call by result (the value of the formal parameter is copied into the actual parameter when the subroutine returns). For parameters of most constructed types, however, Ada specifically permits an implementation to pass either values or references. In most languages, these two different mechanisms would lead to different semantics: changes made to an in out parameter that is passed by reference will affect the actual parameter immediately; changes made to an in out parameter that is passed by value will not affect the actual parameter until the subroutine returns. As noted in Example 9.12, the difference can lead to different behavior in the presence ofaliases.
隐藏引用和值/结果之间的区别的一种可能方法是禁止创建别名,就像 Euclid 所做的那样。Ada 采取了一种更简单的策略:如果程序能够区分 out参数中基于值和引用的实现(非标量、非指针),则该程序被认为是错误的— 虽然不正确,但语言实现不需要捕捉到。
One possible way to hide the distinction between reference and value/result would be to outlaw the creation of aliases, as Euclid does. Ada takes a simpler tack: a program that can tell the difference between value and reference-based implementations of (nonscalar, nonpointer) in out parameters is said to be erroneous—incorrect, but in a way that the language implementation is not required to catch.
Ada 的参数传递语义允许使用一组模式,不仅用于子程序参数,还用于并发执行任务之间的通信(将在第 13 章中讨论)。当任务在单独的机器上执行且没有共同的内存时,传递实际参数的地址不是一个实用的选择。大多数 Ada 编译器通过引用将大参数传递给子程序;它们通过复制将它们传递给任务的入口点。
Ada's semantics for parameter passing allow a single set of modes to be used not only for subroutine parameters but also for communication among concurrently executing tasks (to be discussed in Chapter 13). When tasks are executing on separate machines, with no memory in common, passing the address of an actual parameter is not a practical option. Most Ada compilers pass large arguments to subroutines by reference; they pass them to the entry points of tasks by copying.
与 C 一样,C++ 参数可以声明为const,以确保它不会被修改。对于大型类型, C++ 中的const引用参数提供与 Modula-3 的READONLY参数相同的速度和安全性组合:它们可以通过地址传递,并且不能被调用的例程更改。
As in C, a C++ parameter can be declared to be const to ensure that it is not modified. For large types, const reference parameters in C++ provide the same combination of speed and safety found in the READONLY parameters of Modula-3: they can be passed by address, and cannot be changed by the called routine.
需要注意的是,从函数返回引用的功能在 C++ 中并不是新特性:Algol 68 提供了相同的功能。C++ 的面向对象特性及其运算符重载使引用返回特别有用。
It should be noted that the ability to return references from functions is not new in C++: Algol 68 provides the same capability. The object-oriented features of C++, and its operator overloading, make reference returns particularly useful.
在某些情况下,程序员可能知道在将值作为参数传递后永远不会使用,但编译器可能无法推断出这一事实。为了强制使用移动构造函数,程序员可以将值包装在对标准库移动例程的调用中:
In some cases, the programmer may know that a value will never be used after passing it as a parameter, but the compiler may be unable to deduce this fact. To force the use of a move constructor, the programmer can wrap the value in a call to the standard library move routine:
移动例程不生成任何代码:它实际上是一种强制转换。如果程序实际上包含对o3的后续使用,则行为未定义。
The move routine generates no code: it is, in effect, a cast. Behavior is undefined if the program actually does contain a subsequent use of o3.
和常规引用一样,右值引用可用于 C++ 中任意变量的声明。实际上,它们很少出现在移动构造函数和类似的移动赋值方法(重载 = 运算符)的参数之外。
Like regular references, r-value references can be used in the declaration of arbitrary variables in C++. In practice, they seldom appear outside the parameters of move constructors and the analogous move assignment methods, which overload the = operator.
在面向对象语言中,通过将方法及其“环境”打包在显式对象中,可以近似子程序闭包的行为,即使没有嵌套子程序。我们在第 3.6.3 节中描述了这些对象闭包,特别指出了它们与 lambda 表达式和 C++11 中的标准函数类的集成。因为它们是普通对象,所以对象闭包不需要特殊机制将它们作为参数传递或将它们存储在变量中。
In object-oriented languages, one can approximate the behavior of a subroutine closure, even without nested subroutines, by packaging a method and its “environment” within an explicit object. We described these object closures in Section 3.6.3, noting in particular their integration with lambda expressions and the standard function class in C++11. Because they are ordinary objects, object closures require no special mechanisms to pass them as parameters or to store them in variables.
C# 的委托扩展了对象闭包的概念,从而提供类型安全,而不受继承的限制。委托不仅可以通过指定的对象方法(包含 C++ 和 Java 的对象闭包)实例化,还可以通过静态函数(包含 C 和 C++ 的子例程指针)或匿名嵌套委托或 lambda 表达式(包含真正的子例程闭包)实例化。如果匿名委托或 lambda 表达式引用周围方法中声明的对象,则这些对象具有无限范围。最后,正如我们将在第 9.6.2 节中看到的那样,C# 委托实际上可以包含一个闭包列表,在这种情况下,调用委托的效果是依次调用列表中的所有条目。(这种行为通常只有当每个条目都有void返回类型时才有意义。它主要用于处理事件。)
The delegates of C# extend the notion of object closures to provide type safety without the restrictions of inheritance. A delegate can be instantiated not only with a specified object method (subsuming the object closures of C++ and Java) but also with a static function (subsuming the subroutine pointers of C and C++) or with an anonymous nested delegate or lambda expression (subsuming true subroutine closures). If an anonymous delegate or lambda expression refers to objects declared in the surrounding method, then those objects have unlimited extent. Finally, as we shall see in Section 9.6.2, a C# delegate can actually contain a list of closures, in which case calling the delegate has the effect of calling all the entries on the list, in turn. (This behavior generally makes sense only when each entry has a void return type. It is used primarily when processing events.)
显式子程序参数并不是唯一需要将闭包作为参数传递的语言特性。通常,当参数的最终使用需要恢复先前的引用环境时,语言实现必须传递闭包。Algol 60 和 Simula 的按名称调用参数、Algol 60 和 Algol 68 的标签参数以及Miranda、Haskell 和 R 的按需要调用参数中都有有趣的例子。
Explicit subroutine parameters are not the only language feature that requires a closure to be passed as a parameter. In general, a language implementation must pass a closure whenever the eventual use of the parameter requires the restoration of a previous referencing environment. Interesting examples occur in the call-by-name parameters of Algol 60 and Simula, the label parameters of Algol 60 and Algol 68, and the call-by-need parameters of Miranda, Haskell, and R.
更深入地
IN MORE DEPTH
我们在配套网站上更详细地讨论了按名称调用。当 Algol 60 被定义时,大多数程序员都使用汇编语言进行编程(Fortran 只有几年的历史,而 Lisp 则更新颖)。当时的汇编语言大量使用宏,Algol 设计人员自然而然地提出了一种模仿宏行为的参数传递机制,即正常顺序参数求值(第 6.6.2 节)。考虑到汇编语言中的常见做法,允许goto跳转到作为参数传递的标签也是很自然的。
We consider call by name in more detail on the companion site. When Algol 60 was defined, most programmers programmed in assembly language (Fortran was only a few years old, and Lisp was even newer). The assembly languages of the day made heavy use of macros, and it was natural for the Algol designers to propose a parameter-passing mechanism that mimicked the behavior of macros, namely normal-order argument evaluation (Section 6.6.2). It was also natural, given common practice in assembly language, to allow a goto to jump to a label that was passed as a parameter.
按名称调用参数有一些有趣且强大的应用,但它们比人们最初想象的更难实现(并且使用起来更昂贵):它们需要传递闭包,有时称为thunk。标签参数通常也由闭包实现。按名称调用和标签参数都倾向于导致难以理解的代码;现代语言通常鼓励程序员改用显式的形式子程序和结构化异常。值得注意的是,大多数反对按名称调用的论点在纯函数式代码中都消失了,其中副作用自由确保参数的值无论何时评估都始终相同。利用这一观察,Haskell(及其前身 Miranda)对所有参数采用正常顺序评估。
Call-by-name parameters have some interesting and powerful applications, but they are more difficult to implement (and more expensive to use) than one might at first expect: they require the passing of closures, sometimes referred to as thunks. Label parameters are typically implemented by closures as well. Both call-by-name and label parameters tend to lead to inscrutable code; modern languages typically encourage programmers to use explicit formal subroutines and structured exceptions instead. Significantly, most of the arguments against call by name disappear in purely functional code, where side-effect freedom ensures that the value of a parameter will always be the same regardless of when it is evaluated. Leveraging this observation, Haskell (and its predecessor Miranda) employs normal-order evaluation for all parameters.
图 9.3总结了常见的参数传递模式。本节我们将研究参数传递的其他方面。
Figure 9.3 contains a summary of the common parameter-passing modes. In this subsection we examine other aspects of parameter passing.
在3.3.6 节中,我们注意到默认参数为改变子程序的行为提供了一种有吸引力的动态作用域替代方案。默认参数是调用者不必提供的参数;如果缺少该参数,则将使用预先设定的默认值。
In Section 3.3.6, we noted that default parameters provide an attractive alternative to dynamic scope for changing the behavior of a subroutine. A default parameter is one that need not necessarily be provided by the caller; if it is missing, then a preestablished default value will be used instead.
包括 Lisp、C 及其后代以及大多数脚本语言在内的多种语言都允许用户定义采用可变数量参数的子例程。此类子例程的示例可在 C-8.7.3 节中找到: C 的stdio I/O 库的printf和scanf函数。在 C 中,printf可以声明如下:
Several languages, including Lisp, C and its descendants, and most of the scripting languages, allow the user to define subroutines that take a variable number of arguments. Examples of such subroutines can be found in Section C-8.7.3: the printf and scanf functions of C's stdio I/O library. In C, printf can be declared as follows:
函数头中的省略号 (…) 是语言语法的一部分。它表示格式后面有附加参数,但未指定其类型和数量。由于 C 和 C++ 是静态类型的,因此附加参数不是类型安全的。然而,由于动态类型,它们在 Common Lisp 和脚本语言中是类型安全的。
The ellipsis (…) in the function header is a part of the language syntax. It indicates that there are additional parameters following the format, but that their types and numbers are unspecified. Since C and C++ are statically typed, additional parameters are not type safe. They are type safe in Common Lisp and the scripting languages, however, thanks to dynamic typing.
函数指示要返回的值的语法差异很大。在 Lisp、ML 和 Algol 68 等不区分表达式和语句的语言中,函数的值只是其主体的值,而主体本身就是一个表达式。
The syntax by which a function indicates the value to be returned varies greatly. In languages like Lisp, ML, and Algol 68, which do not distinguish between expressions and statements, the value of a function is simply the value of its body, which is itself an expression.
Fortran 将子程序的终止与返回值的指定分开:它通过分配给函数名称来指定返回值,并且具有不带任何参数的返回语句。
Fortran separates termination of a subroutine from the specification of return values: it specifies the return value by assigning to the function name, and has a return statement that takes no arguments.
在函数式语言中,将子程序作为闭包返回是很常见的。许多命令式语言也允许这样做。C 没有闭包,但允许函数返回指向子程序的指针。
In functional languages, it is commonplace to return a subroutine as a closure. Many imperative languages permit this as well. C has no closures, but allows a function to return a pointer to a subroutine.
我们在前面的章节中多次提到了异常处理机制。我们之所以推迟到现在才详细讨论这些机制,是因为异常处理通常需要语言实现来“展开”子程序调用堆栈。
Several times in the preceding chapters and sections we have referred to exception-handling mechanisms. We have delayed detailed discussion of these mechanisms until now because exception handling generally requires the language implementation to “unwind” the subroutine call stack.
异常可以定义为程序执行过程中出现的意外(或至少是不寻常)情况,并且无法在本地上下文中轻松处理。它可能由语言实现自动检测到,或者程序可能显式地引发或抛出它(这两个术语是同义词)。最常见的异常是各种运行时错误。例如,在 I/O 库中,输入例程可能在读取请求的值之前遇到文件末尾,或者它可能在文件末尾找到标点符号或字母当输入数字时,程序会返回错误。为了在没有异常处理机制的情况下处理此类错误,程序员基本上有三个选择,但没有一个是完全令人满意的:
An exception can be defined as an unexpected—or at least unusual—condition that arises during program execution, and that cannot easily be handled in the local context. It may be detected automatically by the language implementation, or the program may raise or throw it explicitly (the two terms are synonymous). The most common exceptions are various sorts of run-time errors. In an I/O library, for example, an input routine may encounter the end of its file before it can read a requested value, or it may find punctuation marks or letters on the input when it is expecting digits. To cope with such errors without an exception-handling mechanism, the programmer has basically three options, none of which is entirely satisfactory:
1. “Invent” a value that can be used by the caller when a real value could not be returned.
2. 向调用者返回一个显式的“状态”值,调用者必须在每次调用后检查该值。状态通常通过一个额外的显式参数传递。在某些语言中,常规返回值和状态可能会作为元组一起返回。
2. Return an explicit “status” value to the caller, who must inspect it after every call. Most often, the status is passed through an extra, explicit parameter. In some languages, the regular return value and the status may be returned together as a tuple.
3. 依靠调用者传递一个闭包(在支持它们的语言中)来进行错误处理例程,当正常例程遇到问题时可以调用该例程。
3. Rely on the caller to pass a closure (in languages that support them) for an error-handling routine that the normal routine can call when it runs into trouble.
在某些情况下,第一个选项是可行的,但在一般情况下不起作用。选项 2 和 3 往往会使程序变得混乱,并带来我们通常希望避免的开销。选项 2 中的测试尤其令人反感:它们掩盖了通常情况下的正常事件流。由于它们非常繁琐和重复,因此它们也是常见的错误来源;人们很容易忘记所需的测试。异常处理机制通过将错误检查代码“移出行外”来解决这些问题,允许简单地指定正常情况,并在适当的时候安排控制分支到处理程序。
The first of these options is fine in certain cases, but does not work in the general case. Options 2 and 3 tend to clutter up the program, and impose overhead that we should like to avoid in the common case. The tests in option 2 are particularly offensive: they obscure the normal flow of events in the common case. Because they are so tedious and repetitive, they are also a common source of errors; one can easily forget a needed test. Exception-handling mechanisms address these issues by moving error-checking code “out of line,” allowing the normal case to be specified simply, and arranging for control to branch to a handler when appropriate.
如果调用 PL/I 异常处理程序然后“返回”(即不执行 GOTO到程序中的其他位置),则会发生以下两种情况之一。对于语言设计者认为是致命的异常,程序本身将终止。对于“可恢复”异常,执行将在发生异常的语句之后的语句处恢复。不幸的是,PL/I 的经验表明,处理程序与异常的动态绑定以及发生异常的代码的自动恢复都令人困惑且容易出错。
If a PL/I exception handler is invoked and then “returns” (i.e., does not perform a GOTO to somewhere else in the program), then one of two things will happen. For exceptions that the language designers considered to be fatal, the program itself will terminate. For “recoverable” exceptions, execution will resume at the statement following the one in which the exception occurred. Unfortunately, experience with PL/I revealed that both the dynamic binding of handlers to exceptions and the automatic resumption of code in which an exception occurred were confusing and error-prone.
从某种意义上说,异常处理对子程序调用顺序的依赖性可以被视为一种动态绑定形式,但它比 PL/I 中的形式要严格得多。与其说调用例程中的处理程序已动态绑定到被调用例程中的错误,我们更愿意说处理程序在词汇上绑定到调用被调用例程的表达式或语句。然后,被调用例程中未处理的异常可以建模为“异常返回”;它会导致调用表达式或语句引发异常,而该异常又在其子例程中在词汇上进行处理。
In a sense, the dependence of exception handling on the order of subroutine calls might be considered a form of dynamic binding, but it is a much more restricted form than is found in PL/I. Rather than say that a handler in a calling routine has been dynamically bound to an error in a called routine, we prefer to say that the handler is lexically bound to the expression or statement that calls the called routine. An exception that is not handled inside a called routine can then be modeled as an “exceptional return”; it causes the calling expression or statement to raise an exception, which is again handled lexically within its subroutine.
实际上,异常处理程序倾向于执行三种操作。首先,理想情况下,处理程序将以允许程序恢复并继续执行的方式补偿异常。例如,为了响应存储管理例程中的“内存不足”异常,处理程序可能会要求操作系统为应用程序分配更多空间,之后它可以完成请求的操作。其次,当给定代码块中发生异常但无法在本地处理时,通常重要的是声明一个本地处理程序来清理本地块中分配的任何资源,然后“重新引发”异常,以便它将继续传播回可以(希望)恢复的处理程序。第三,如果无法恢复,处理程序至少可以在程序终止之前打印一条有用的错误消息。
In practice, exception handlers tend to perform three kinds of operations. First, ideally, a handler will compensate for the exception in a way that allows the program to recover and continue execution. For example, in response to an “out of memory” exception in a storage management routine, a handler might ask the operating system to allocate additional space to the application, after which it could complete the requested operation. Second, when an exception occurs in a given block of code but cannot be handled locally, it is often important to declare a local handler that cleans up any resources allocated in the local block, and then “reraises” the exception, so that it will continue to propagate back to a handler that can (hopefully) recover. Third, if recovery is not possible, a handler can at least print a helpful error message before the program terminates.
如第 6.2.1 节所述,异常与多级返回的概念相关,但又有区别。执行多级返回的例程按预期运行;用 Eiffel 术语来说,它正在履行其契约。引发异常的例程未按预期运行;它无法履行其契约。Common Lisp 和 Ruby 区分这两个相关概念,但大多数语言没有;在大多数情况下,多级返回需要外部调用者提供一个简单的处理程序。
As discussed in Section 6.2.1, exceptions are related to, but distinct from, the notion of multilevel returns. A routine that performs a multilevel return is functioning as expected; in Eiffel terminology, it is fulfilling its contract. A routine that raises an exception is not functioning as expected; it cannot fulfill its contract. Common Lisp and Ruby distinguish between these two related concepts, but most languages do not; in most, a multilevel return requires the outer caller to provide a trivial handler.
Common Lisp 的不同寻常之处还在于它提供了四种不同版本的异常处理机制。其中两个版本提供通常的“异常返回”语义;其他版本旨在修复问题并重新开始某个动态封闭表达式的求值。正交地,两个版本在声明处理程序的引用环境中执行工作;其他版本在异常首次发生的环境中执行工作。后一种选择允许抽象提供几种从异常中恢复的备选策略。然后,抽象的用户可以动态指定在给定上下文中应使用哪种策略。我们将在练习 9.22和探索 9.43中进一步考虑 Common Lisp 。在处理程序环境中执行工作的“异常返回”机制称为handler-case;它提供的语义与大多数其他现代语言相当。
Common Lisp is also unusual in providing four different versions of its exception-handling mechanism. Two of these provide the usual “exceptional return” semantics; the others are designed to repair the problem and restart evaluation of some dynamically enclosing expression. Orthogonally, two perform their work in the referencing environment where the handler is declared; the others perform their work in the environment where the exception first arises. The latter option allows an abstraction to provide several alternative strategies for recovery from exceptions. The user of the abstraction can then specify, dynamically, which of these strategies should be used in a given context. We will consider Common Lisp further in Exercise 9.22 and Exploration 9.43. The “exceptional return” mechanism, with work performed in the environment of the handler, is known as handler-case; it provides semantics comparable to those of most other modern languages.
在许多语言中,动态语义错误会自动导致异常,然后程序可以捕获这些异常。程序员还可以定义其他特定于应用程序的异常。预定义异常的示例包括算术溢出、除以零、输入时文件末尾、下标和子范围错误以及空指针取消引用。将这些定义为异常(而不是致命错误)的理由是它们可能出现在某些有效程序中。在大多数语言中,一些其他动态错误(例如,从尚未指定返回值的子程序返回)仍然是致命的。在 C++ 和 Common Lisp 中,所有异常都是程序员定义的。在 PHP 中,set_error_handler函数可用于将内置语义错误转换为普通异常。在 Ada 中,一些预定义异常可以通过编译指示来抑制。
In many languages, dynamic semantic errors automatically result in exceptions, which the program can then catch. The programmer can also define additional, application-specific exceptions. Examples of predefined exceptions include arithmetic overflow, division by zero, end-of-file on input, subscript and subrange errors, and null pointer dereference. The rationale for defining these as exceptions (rather than as fatal errors) is that they may arise in certain valid programs. Some other dynamic errors (e.g., return from a subroutine that has not yet designated a return value) are still fatal in most languages. In C++ and Common Lisp, exceptions are all programmer defined. In PHP, the set_error_handler function can be used to turn built-in semantic errors into ordinary exceptions. In Ada, some of the predefined exceptions can be suppressed by means of a pragma.
如果子程序引发异常但未在内部捕获它,则它可能会以意外的方式“返回”。这种可能性是子程序与程序其余部分接口的重要组成部分。因此,包括 Modula-3、C++ 和 Java 在内的几种语言在每个子程序头中包含一个列表可能传播出子程序的异常。此列表在 Modula-3 中是强制性的:如果发生未在标头中出现且未在内部捕获的异常,则为运行时错误。此列表在 C++ 中是可选的:如果出现,则语义与 Modula-3 中的相同;如果省略,则允许所有异常传播。Java 采用一种折中方法:它将异常分为“已检查”和“未检查”类别。已检查异常必须在子程序标头中声明;未检查异常则不需要。未检查异常通常是大多数程序希望成为致命错误的运行时错误(例如,下标越界)——因此在每个函数中声明它会很麻烦——但如果它们出现在库例程中,高度健壮的程序可能希望捕获它们。
If a subroutine raises an exception but does not catch it internally, it may “return” in an unexpected way. This possibility is an important part of the routine's interface to the rest of the program. Consequently, several languages, including Modula-3, C++, and Java, include in each subroutine header a list of the exceptions that may propagate out of the routine. This list is mandatory in Modula-3: it is a run-time error if an exception arises that does not appear in the header and is not caught internally. The list is optional in C++: if it appears, the semantics are the same as in Modula-3; if it is omitted, all exceptions are permitted to propagate. Java adopts an intermediate approach: it segregates its exceptions into “checked” and “unchecked” categories. Checked exceptions must be declared in subroutine headers; unchecked exceptions need not. Unchecked exceptions are typically run-time errors that most programs will want to be fatal (e.g., subscript out of bounds)—and that would therefore be a nuisance to declare in every function—but that a highly robust program may want to catch if they occur in library routines.
在递归子程序中声明的异常将在运行时被该异常的最内层处理程序捕获。如果异常传播到声明它的范围之外,则处理程序将无法再命名它,因此只能由“全部捕获”处理程序捕获。在具有并发性的语言中,一个必须考虑如果在并发控制线程的最外层未处理异常,将会发生什么情况。在 Modula-3 和 C++ 中,整个程序会异常终止;在 Ada 和 Java 中,受影响的线程会安静地终止;在 C# 中,行为由实现定义。
An exception that is declared in a recursive subroutine will be caught by the innermost handler for that exception at run time. If an exception propagates out of the scope in which it was declared, it can no longer be named by a handler, and thus can be caught only by a “catch-all” handler. In a language with concurrency, one must consider what will happen if an exception is not handled at the outermost level of a concurrent thread of control. In Modula-3 and C++, the entire program terminates abnormally; in Ada and Java, the affected thread terminates quietly; in C#, the behavior is implementation defined.
在搜索匹配的处理程序的过程中,异常处理机制必须通过回收引发异常的任何子例程的框架来“展开”运行时堆栈。回收框架不仅需要从堆栈中弹出其空间,还需要恢复作为调用序列的一部分保存的任何寄存器。(我们将在第9.4.3 节中更详细地讨论实现问题。)
In the process of searching for a matching handler, the exception-handling mechanism must “unwind” the run-time stack by reclaiming the frames of any subroutines from which the exception escapes. Reclaiming a frame requires not only that its space be popped from the stack but also that any registers that were saved as part of the calling sequence be restored. (We discuss implementation issues in more detail in Section 9.4.3.)
这种实现的问题在于,在常见情况下会产生运行时开销。每个受保护的块和每个子例程都以将处理程序推送到处理程序列表的代码开始,并以将其从列表中弹出的代码结束。我们通常可以做得更好。
The problem with this implementation is that it incurs run-time overhead in the common case. Every protected block and every subroutine begins with code to push a handler onto the handler list, and ends with code to pop it back off the list. We can usually do better.
处理程序列表的唯一真正目的是确定哪个处理程序处于活动状态。由于源代码块往往会转换为连续的机器语言指令块,因此我们可以以编译时生成的表的形式捕获处理程序和受保护块之间的对应关系。表中的每个条目包含两个字段:代码块的起始地址和相应处理程序的地址。该表按第一个字段排序。发生异常时,语言运行时系统使用程序计数器作为键在表中执行二进制搜索,以找到当前块的处理程序。如果该处理程序重新引发异常,则重复该过程:处理程序本身是代码块,可以在表中找到。唯一的微妙之处在于与子例程外传播相关的隐式处理程序的情况:这样的处理程序必须确保重新引发的代码使用子例程的返回地址而不是当前程序计数器作为表查找的键。
The only real purpose of the handler list is to determine which handler is active. Since blocks of source code tend to translate into contiguous blocks of machine language instructions, we can capture the correspondence between handlers and protected blocks in the form of a table generated at compile time. Each entry in the table contains two fields: the starting address of a block of code and the address of the corresponding handler. The table is sorted on the first field. When an exception occurs, the language run-time system performs binary search in the table, using the program counter as key, to find the handler for the current block. If that handler reraises the exception, the process repeats: handlers themselves are blocks of code, and can be found in the table. The only subtlety arises in the case of the implicit handlers associated with propagation out of subroutines: such a handler must ensure that the reraise code uses the return address of the subroutine, rather than the current program counter, as the key for table lookup.
在第二种实现中,引发异常的成本更高,是处理程序总数的对数倍。但只有在实际发生异常时才需要支付此成本。假设异常是不寻常的事件,则对性能的净影响显然是有益的:常见情况下的成本为零。在纯粹的形式下,基于表的方法要求编译器能够访问整个程序,或者链接器提供将子表粘合在一起的机制。如果代码片段是独立编译的,我们可以采用混合方法,其中编译器为每个子例程创建一个单独的表,并且每个堆栈框架包含指向相应表的指针。
The cost of raising an exception is higher in this second implementation, by a factor logarithmic in the total number of handlers. But this cost is paid only when an exception actually occurs. Assuming that exceptions are unusual events, the net impact on performance is clearly beneficial: the cost in the common case is zero. In its pure form the table-based approach requires that the compiler have access to the entire program, or that the linker provide a mechanism to glue subtables together. If code fragments are compiled independently, we can employ a hybrid approach in which the compiler creates a separate table for each subroutine, and each stack frame contains a pointer to the appropriate table.
值得注意的是,有时可以使用不提供内置异常的语言来模拟异常。在第 6.2 节中,我们注意到 Pascal 允许goto到当前子程序之外的标签,Algol 60 允许将标签作为参数传递,而 PL/I 允许将它们存储在变量中。这些机制允许程序以非常非结构化的方式退出深度嵌套的上下文。
It is worth noting that exceptions can sometimes be simulated in a language that does not provide them as a built-in. In Section 6.2 we noted that Pascal permitted gotos to labels outside the current subroutine, that Algol 60 allowed labels to be passed as parameters, and that PL/I allowed them to be stored in variables. These mechanisms permit the program to escape from a deeply nested context, but in a very unstructured way.
在 Scheme 和 Ruby 等语言的call-with-current-continuation (call-cc)例程中可以找到一种更结构化的替代方案。如第 6.2.2 节所述,call-cc接受一个参数f,该参数本身是一个函数。它调用f ,并将一个延续c (闭包)作为参数传递,该延续 c 捕获当前程序计数器和引用环境。在未来的任何时候,f都可以调用c来重新建立保存的环境。如果进行了嵌套调用,控制权会放弃它们,就像处理异常一样。如果我们将受保护的块及其处理程序表示为闭包(lambda 表达式),则call-cc可用于维护一个延续堆栈,应该跳转到该堆栈来模拟raise/throw 。我们将在练习 9.18中进一步探讨这个选项。
A more structured alternative can be found in the call-with-current-continuation (call-cc) routine of languages like Scheme and Ruby. As described in Section 6.2.2, call-cc takes a single argument f, which is itself a function. It calls f, passing as argument a continuation c (a closure) that captures the current program counter and referencing environment. At any point in the future, f can call c to reestablish the saved environment. If nested calls have been made, control abandons them, as it does with exceptions. If we represent a protected block and its handlers as closures (lambda expressions), call-cc can be used to maintain a stack of continuations to which one should jump to emulate raise/throw. We explore this option further in Exercise 9.18.
setjmp和longjmp的典型实现将当前机器寄存器保存在setjmp缓冲区中,并在longjmp中恢复它们。没有处理程序列表;实现不是“展开”堆栈,而是通过恢复sp和fp的旧值来抛出所有嵌套框架。这种方法的问题在于,处理程序开头的寄存器内容不反映受保护代码成功完成部分的效果:它们是在该代码开始运行之前保存的。对已成功完成的变量的任何更改写入内存的所有数据将在处理程序中可见,但缓存在寄存器中的更改将会丢失。为了解决这个限制,C 语言允许程序员将某些变量指定为volatile。 volatile 变量在内存中的值可以“自发”更改,例如由于 I/O 设备活动或并发控制线程而更改。C 语言实现需要在写入 volatile 变量时将其存储到内存中,并在读取它们时从内存加载它们。如果处理程序需要查看可能被受保护代码修改的变量的更改,则程序员必须在变量的声明中包含volatile关键字。
The typical implementation of setjmp and longjmp saves the current machine registers in the setjmp buffer, and restores them in longjmp. There is no list of handlers; rather than “unwinding” the stack, the implementation simply tosses all the nested frames by restoring old values of the sp and fp. The problem with this approach is that the register contents at the beginning of the handler do not reflect the effects of the successfully completed portion of the protected code: they were saved before that code began to run. Any changes to variables that have been written through to memory will be visible in the handler, but changes that were cached in registers will be lost. To address this limitation, C allows the programmer to specify that certain variables are volatile. A volatile variable is one whose value in memory can change “spontaneously,” for example as the result of activity by an I/O device or a concurrent thread of control. C implementations are required to store volatile variables to memory whenever they are written, and to load them from memory whenever they are read. If a handler needs to see changes to a variable that may be modified by the protected code, then the programmer must include the volatile keyword in the variable's declaration.
了解了运行时堆栈的布局后,我们现在可以考虑更通用的控制抽象的实现——特别是协程。与延续一样,协程由闭包(代码地址和引用环境)表示,我们可以通过非本地 goto 跳转到其中,在本例中,这是一种称为传输的特殊操作。这两个抽象之间的主要区别在于,延续是一个常量——一旦创建就不会改变——而协程每次运行时都会改变。当我们转到延续时,旧的程序计数器会丢失,除非我们明确创建一个新的延续来保存它。当我们从一个协程转移到另一个协程时,我们的旧程序计数器已保存:我们离开的协程将更新以反映它。因此,如果我们多次执行 goto进入同一个延续,则每次跳转都将从完全相同的位置开始,但如果我们多次执行转移到同一个协程,则每次跳转都将从前一次跳转停止的位置开始。
Given an understanding of the layout of the run-time stack, we can now consider the implementation of more general control abstractions—coroutines in particular. Like a continuation, a coroutine is represented by a closure (a code address and a referencing environment), into which we can jump by means of a nonlocal goto, in this case a special operation known as transfer. The principal difference between the two abstractions is that a continuation is a constant—it does not change once created—while a coroutine changes every time it runs. When we goto a continuation, our old program counter is lost, unless we explicitly create a new continuation to hold it. When we transfer from one coroutine to another, our old program counter is saved: the coroutine we are leaving is updated to reflect it. Thus, if we perform a goto into the same continuation multiple times, each jump will start at precisely the same location, but if we perform a transfer into the same coroutine multiple times, each jump will take up where the previous one left off.
实际上,协程是并发存在的执行上下文,但每次只执行一个,并且通过名称明确地相互转移控制权。协程可用于实现迭代器(第 6.5.3 节)和线程(将在第 13 章中讨论)。它们本身也很有用,特别是对于某些类型的服务器和离散事件模拟。从历史上看,线程最早出现于 Algol 68。今天,它们可以在 Ada、Java、C#、C++、Python、Ruby、Haskell、Go 和 Scala 等许多语言中找到。它们也通常通过库包在语言本身之外提供(尽管语法和语义不那么吸引人)。协程作为用户级编程抽象不太常见。从历史上看,提供它们的两种最重要的语言是 Simula 和 Modula-2。在以下小节中,我们将重点介绍协程的实现以及(在配套站点上)它们在迭代器(第 C-9.5.3 节)和离散事件模拟(第 C-9.5.4 节)中的使用。
In effect, coroutines are execution contexts that exist concurrently, but that execute one at a time, and that transfer control to each other explicitly, by name. Coroutines can be used to implement iterators (Section 6.5.3) and threads (to be discussed in Chapter 13). They are also useful in their own right, particularly for certain kinds of servers, and for discrete event simulation. Threads have appeared, historically, as far back as Algol 68. Today they can be found in Ada, Java, C#, C++, Python, Ruby, Haskell, Go, and Scala, among many others. They are also commonly provided (though with somewhat less attractive syntax and semantics) outside the language proper by means of library packages. Coroutines are less common as a user-level programming abstraction. Historically, the two most important languages to provide them were Simula and Modula-2. We focus in the following subsections on the implementation of coroutines and (on the companion site) on their use in iterators (Section C-9.5.3) and discrete event simulation (Section C-9.5.4).
由于协程是并发的(即同时启动但未完成),因此协程不能共享单个堆栈:它们的子程序调用和返回作为一个整体,并不是按照后进先出的顺序发生的。 如果每个协程都在词法嵌套的最外层声明(正如 Modula-2 中所要求的那样),那么它们的堆栈是完全不相交的:它们共享的唯一对象是全局的,因此是静态分配的。 大多数操作系统都可以轻松分配一个堆栈,并在执行过程中根据需要增加其虚拟地址空间部分。 分配任意数量的此类堆栈并不容易; 协程的空间在历史上一直是一个实现挑战,至少在虚拟地址空间有限的机器上是这样(64 位架构通过使虚拟地址相对充足,缓解了这个问题)。
Because they are concurrent (i.e., simultaneously started but not completed), coroutines cannot share a single stack: their subroutine calls and returns, taken as a whole, do not occur in last-in-first-out order. If each coroutine is declared at the outermost level of lexical nesting (as was required in Modula-2), then their stacks are entirely disjoint: the only objects they share are global, and thus statically allocated. Most operating systems make it easy to allocate one stack, and to increase its portion of the virtual address space as necessary during execution. It is not as easy to allocate an arbitrary number of such stacks; space for coroutines was historically something of an implementation challenge, at least on machines with limited virtual address space (64-bit architectures ease the problem, by making virtual addresses relatively plentiful).
最简单的方法是给每个协程分配固定数量的静态堆栈空间。这种方法在 Modula-2 中被采用,它要求程序员在初始化协程时指定堆栈的大小和位置。如果协程需要额外的空间,则会出现运行时错误。一些 Modula-2 实现会捕获溢出并暂停并显示错误消息;其他实现则会显示异常行为。如果协程使用的(虚拟)空间少于给定的空间,则多余的空间就浪费了。
The simplest approach is to give each coroutine a fixed amount of statically allocated stack space. This approach was adopted in Modula-2, which required the programmer to specify the size and location of the stack when initializing a coroutine. It was a run-time error for the coroutine to need additional space. Some Modula-2 implementations would catch the overflow and halt with an error message; others would display abnormal behavior. If the coroutine used less (virtual) space than it was given, the excess was simply wasted.
如果像在大多数函数式语言实现中那样从堆中分配堆栈帧,则可以避免溢出和内部碎片问题。同时,每个子例程调用的开销会增加。一个折衷方案是将堆栈分配为较大的固定大小的“块”。每次调用时,子例程调用序列都会检查当前块中是否有足够的空间来容纳被调用例程的帧。如果没有,则分配另一个块并将帧放在那里。每次子例程返回时,结尾代码都会检查当前帧是否是其块中的最后一个。如果是,则将块返回到“空闲块”池。为了减少调用开销,如果编译器能够验证子例程在返回之前不会执行传输,则可以使用普通的中央堆栈 [ Sco91 ]。
If stack frames are allocated from the heap, as they are in most functional language implementations, then the problems of overflow and internal fragmentation are avoided. At the same time, the overhead of each subroutine call increases. An intermediate option is to allocate the stack in large, fixed-size “chunks.” At each call, the subroutine calling sequence checks to see whether there is sufficient space in the current chunk to hold the frame of the called routine. If not, another chunk is allocated and the frame is put there instead. At each subroutine return, the epilogue code checks to see whether the current frame is the last one in its chunk. If so, the chunk is returned to a “free chunk” pool. To reduce the overhead of calls, the compiler can use the ordinary central stack if it is able to verify that a subroutine will not perform a transfer before returning [Sco91].
要从一个协程转移到另一个协程,运行时系统必须更改程序计数器 (PC)、堆栈和处理器寄存器的内容。这些更改封装在转移操作中:一个协程调用transfer;返回一个不同的协程。由于更改发生在transfer内部,因此将 PC 从一个协程更改为另一个协程只需记住正确的返回地址:旧协程从程序中的一个位置调用transfer;新协程返回到一个可能不同的位置。如果transfer将其返回地址保存在堆栈中,则 PC 将作为更改堆栈的副作用自动更改。
To transfer from one coroutine to another, the run-time system must change the program counter (PC), the stack, and the contents of the processor's registers. These changes are encapsulated in the transfer operation: one coroutine calls transfer; a different one returns. Because the change happens inside transfer, changing the PC from one coroutine to another simply amounts to remembering the right return address: the old coroutine calls transfer from one location in the program; the new coroutine returns to a potentially different location. If transfer saves its return address in the stack, then the PC will change automatically as a side effect of changing stacks.
表示协程或线程的数据结构称为上下文块。在简单的协程包中,上下文块包含单个值:协程的sp(截至其最近一次传输)。(线程包通常会在上下文块中放置其他信息,例如优先级指示或将线程链接到各种调度队列的指针。一些协程或线程包选择将寄存器保存在上下文块中,而不是堆栈顶部;这两种方法都可以。)
The data structure that represents a coroutine or thread is called a context block. In a simple coroutine package, the context block contains a single value: the coroutine's sp as of its most recent transfer. (A thread package generally places additional information in the context block, such as an indication of priority, or pointers to link the thread onto various scheduling queues. Some coroutine or thread packages choose to save registers in the context block, rather than at the top of the stack; either approach works fine.)
在 Modula-2 中,协程创建例程会将协程的堆栈初始化为类似于transfer的框架,并初始化返回地址和寄存器内容以允许“返回”到协程代码的开头。创建例程会将上下文块中的sp值设置为指向这个人工框架,并返回指向上下文块的指针。要开始执行协程,需要将一些现有例程传输到它。
In Modula-2, the coroutine creation routine would initialize the coroutine's stack to look like the frame of transfer, with a return address and register contents initialized to permit a “return” into the beginning of the coroutine's code. The creation routine would set the sp value in the context block to point into this artificial frame, and return a pointer to the context block. To begin execution of the coroutine, some existing routine would need to transfer to it.
在 Simula 中(以及示例 9.47中的代码中),协程创建例程将立即开始执行新的协程,就像它是一个子例程一样。在协程完成任何特定于应用程序的初始化后,它将执行分离操作。分离将设置协程堆栈,使其看起来像transfer的框架,并带有指向以下语句的返回地址。然后,它将允许创建例程返回到其自己的调用者。
In Simula (and in the code in Example 9.47), the coroutine creation routine would begin to execute the new coroutine immediately, as if it were a subroutine. After the coroutine completed any application-specific initialization, it would perform a detach operation. Detach would set up the coroutine stack to look like the frame of transfer, with a return address that pointed to the following statement. It would then allow the creation routine to return to its own caller.
在所有情况下,transfer都需要一个指向上下文块的指针作为参数;通过取消引用该指针,它可以找到下一个要运行的协程的sp 。在示例 9.49的代码中,全局(静态)变量称为current_coroutine,它包含一个指向当前正在运行的协程的上下文块的指针。此指针允许transfer找到应保存旧sp 的位置。
In all cases, transfer expects a pointer to a context block as argument; by dereferencing the pointer it can find the sp of the next coroutine to run. A global (static) variable, called current_coroutine in the code of Example 9.49, contains a pointer to the context block of the currently running coroutine. This pointer allows transfer to find the location in which it should save the old sp.
给定一个协程的实现,迭代器几乎是微不足道的:一个协程用于表示主程序;第二个用于表示迭代器。如果迭代器嵌套,则可能需要额外的协程。
Given an implementation of coroutines, iterators are almost trivial: one coroutine is used to represent the main program; a second is used to represent the iterator. Additional coroutines maybe needed if iterators nest.
更深入地
IN MORE DEPTH
更多详细信息请参见配套网站。事实证明,协程对于迭代器实现来说有些过度了。大多数编译器使用两种更简单的替代方案之一。第一种将所有状态保存在单个堆栈中,但有时会在最顶层以外的框架中执行。第二种采用编译时代码转换,以等效迭代器对象透明地替换真正的迭代器。
Additional details appear on the companion site. As it turns out, coroutines are overkill for iterator implementation. Most compilers use one of two simpler alternatives. The first of these keeps all state in a single stack, but sometimes executes in a frame other than the topmost. The second employs a compile-time code transformation to replace true iterators, transparently, with equivalent iterator objects.
协程的最重要应用之一(也是 Simula 的设计和命名目标)是离散事件模拟。模拟通常指的是创建某个现实世界系统的抽象模型,然后使用该模型进行实验以推断现实世界系统的属性的任何过程。当对现实世界进行实验会变得复杂、危险、昂贵或不切实际时,模拟是可取的。离散事件模拟是一种将模型自然地表达为在特定时间发生的事件(通常是各种有趣对象之间的相互作用)的模拟。离散事件模拟通常不适用于连续过程,例如晶体的生长或水在表面的流动,除非这些过程是在单个粒子的级别捕获的。
One of the most important applications of coroutines (and the one for which Simula was designed and named) is discrete event simulation. Simulation in general refers to any process in which we create an abstract model of some real-world system, and then experiment with the model in order to infer properties of the real-world system. Simulation is desirable when experimentation with the real world would be complicated, dangerous, expensive, or otherwise impractical. A discrete event simulation is one in which the model is naturally expressed in terms of events (typically interactions among various interesting objects) that happen at specific times. Discrete event simulation is usually not appropriate for continuous processes, such as the growth of crystals or the flow of water over a surface, unless these processes are captured at the level of individual particles.
更深入地
IN MORE DEPTH
在配套网站上,我们考虑进行交通模拟,其中事件模拟汽车、路口和交通信号灯之间的相互作用。我们为每次驾车出行使用单独的协程。在任何给定时间,我们都会运行具有最早预计到达前方路口时间的协程。我们将不活动的协程保存在按到达时间排序的优先级队列中。
On the companion site we consider a traffic simulation, in which events model interactions among automobiles, intersections, and traffic lights. We use a separate coroutine for each trip to be taken by car. At any given time we run the coroutine with the earliest expected arrival time at an upcoming intersection. We keep inactive coroutines in a priority queue ordered by those arrival times.
事件是正在运行的程序(进程)需要响应的事物,但它发生在程序之外,时间不可预测。事件通常由图形用户界面 (GUI) 系统的输入引起:按键、鼠标移动、按钮点击。它们也可能是网络操作或其他异步 I/O 活动:消息的到达、先前请求的磁盘操作的完成。
An event is something to which a running program (a process) needs to respond, but which occurs outside the program, at an unpredictable time. Events are commonly caused by inputs to a graphical user interface (GUI) system: keystrokes, mouse motions, button clicks. They may also be network operations or other asynchronous I/O activity: the arrival of a message, the completion of a previously requested disk operation.
在C-8.7 节(尤其是 C-8.7.3 节)讨论的 I/O 操作中,我们假设寻找输入的程序会明确请求输入,如果输入尚不可用,则会等待。这种同步(在指定时间)和阻塞(可能导致等待)输入通常是现代具有图形界面的应用程序所不接受的。相反,程序员通常希望在发生给定事件时调用处理程序(一种特殊的子例程)。处理程序有时也称为回调函数,因为运行时系统会回调主程序,而不是从主程序中调用。在面向对象语言中,回调函数可能是某个处理程序对象的方法,而不是静态子例程。
In the I/O operations discussed in Section C-8.7, and in Section C-8.7.3 in particular, we assumed that a program looking for input will request it explicitly, and will wait if it isn't yet available. This sort of synchronous (at a specified time) and blocking (potentially wait-inducing) input is generally not acceptable for modern applications with graphical interfaces. Instead, the programmer usually wants a handler—a special subroutine—to be invoked when a given event occurs. Handlers are sometimes known as callback functions, because the run-time system calls back into the main program instead of being called from it. In an object-oriented language, the callback function maybe a method of some handler object, rather than a static subroutine.
传统上,事件处理程序在顺序编程语言中作为“自发”子例程调用实现,通常使用操作系统在语言本身之外定义和实现的机制。为了准备通过此机制接收事件,程序(称为P )调用setup_handler库例程,将其希望在事件发生时调用的子例程作为参数传递。
Traditionally, event handlers were implemented in sequential programming languages as “spontaneous” subroutine calls, typically using a mechanism defined and implemented by the operating system, outside the language proper. To prepare to receive events through this mechanism, a program—call it P—invokes a setup_handler library routine, passing as argument the subroutine it wants to have invoked when the event occurs.
在硬件层面,P执行期间的异步设备活动将触发中断机制,该机制会保存P的寄存器、切换到不同的堆栈并跳转到 OS 内核中的预定义地址。同样,如果发生中断时其他进程Q正在运行(或者Q本身中的某些操作需要作为事件反映给P),则内核将在其最后一个时间片结束时保存P的状态。无论哪种方式,内核都必须安排调用适当的事件处理程序,尽管P可能位于其代码中通常无法发生子例程调用的位置(例如,它可能位于某个其他子例程的调用序列的中间位置)。
At the hardware level, asynchronous device activity during P's execution will trigger an interrupt mechanism that saves P's registers, switches to a different stack, and jumps to a predefined address in the OS kernel. Similarly, if some other process Q is running when the interrupt occurs (or if some action in Q itself needs to be reflected to P as an event), the kernel will have saved P's state at the end of its last time slice. Either way, the kernel must arrange to invoke the appropriate event handler despite the fact that P may be at a place in its code where a subroutine call cannot normally occur (e.g., it may be halfway through the calling sequence for some other subroutine).
实际上,大多数事件处理程序需要与主程序共享数据结构(否则,它们如何让程序对事件做出任何有趣的响应?)。我们必须小心确保处理程序和主程序都不会看到这些共享结构处于不一致状态。具体来说,我们必须防止处理程序在主程序正在修改数据时查看数据,或者在主程序正在读取数据时修改数据。典型的解决方案是通过将主程序中的代码块与禁用和重新启用信号的内核调用括起来,来同步对这些共享结构的访问。我们将在13.2.4 节中使用类似的机制在协程之上实现线程。更一般的同步形式将出现在13.3 节中。
In practice, most event handlers need to share data structures with the main program (otherwise, how would they get the program to do anything interesting in response to the event?). We must take care to make sure neither the handler nor the main program ever sees these shared structures in an inconsistent state. Specifically, we must prevent a handler from looking at data when the main program is halfway through modifying it, or modifying data when the main program is halfway through reading it. The typical solution is to synchronize access to such shared structures by bracketing blocks of code in the main program with kernel calls that disable and reenable signals. We will use a similar mechanism to implement threads on top of coroutines in Section 13.2.4. More general forms of synchronization will appear in Section 13.3.
在现代编程语言和运行时系统中,事件通常由单独的控制线程处理,而不是由自发的子例程调用处理。使用单独的处理程序线程,输入可以再次同步:处理程序线程进行系统调用以请求下一个事件,并等待其发生。同时,主程序继续执行。如果程序希望能够同时处理多个事件,它可以创建多个处理程序线程,每个线程都会调用内核来等待事件。为了保护共享数据结构的完整性,主程序和处理程序线程通常需要一个成熟的同步机制,如第13.3 节所述:禁用信号是不够的。
In modern programming languages and run-time systems, events are often handled by a separate thread of control, rather than by spontaneous subroutine calls. With a separate handler thread, input can again be synchronous: the handler thread makes a system call to request the next event, and waits for it to occur. Meanwhile, the main program continues to execute. If the program wishes to be able to handle multiple events concurrently, it may create multiple handler threads, each of which calls into the kernel to wait for an event. To protect the integrity of shared data structures, the main program and the handler thread(s) will generally require a full-fledged synchronization mechanism, as discussed in Section 13.3: disabling signals will not suffice.
处理程序执行的操作需要简单而简短,以便处理程序线程可以回调内核以处理另一个事件。如果处理程序花费的时间太长,用户可能会发现应用程序没有响应。如果事件需要启动一些计算要求高的操作,或者可能需要执行额外的 I/O,处理程序可能会创建一个新线程来完成这项工作;或者,它可能将请求传递给某个现有的工作线程。
The action performed by a handler needs to be simple and brief, so the handler thread can call back into the kernel for another event. If the handler takes too long, the user is likely to find the application nonresponsive. If an event needs to initiate something that is computationally demanding, or that may need to perform additional I/O, the handler may create a new thread to do the work; alternatively, it may pass a request to some existing worker thread.
本章重点讨论了控件抽象,特别是子程序。子程序允许程序员将代码封装在一个狭窄的接口后面,然后就可以使用该接口而无需考虑其实现。
This chapter has focused on the subject of control abstraction, and on subroutines in particular. Subroutines allow the programmer to encapsulate code behind a narrow interface, which can then be used without regard to its implementation.
我们从第 9.1 节开始学习子程序,首先回顾了子程序调用堆栈的管理。然后,我们考虑了用于维护堆栈的调用序列,配套站点上有专门介绍显示的额外章节;分别针对 ARM 和 x86 上的 LLVM 和gcc编译器的案例研究;以及SPARC 的寄存器窗口。在简要考虑了内联扩展之后,我们在第 9.3 节中转向参数主题。我们首先考虑了参数传递模式,所有这些模式都是通过传递值、引用或闭包来实现的。我们注意到,语义清晰度和实现速度的目标有时会发生冲突:通过引用传递大参数通常最有效,但由此产生的别名可能会导致程序错误。在第 9.3.3 节中,我们考虑了特殊的参数传递机制,包括默认(可选)参数、命名参数和可变长度参数列表。
We began our study of subroutines in Section 9.1 by reviewing the management of the subroutine call stack. We then considered the calling sequences used to maintain the stack, with extra sections on the companion site devoted to displays; case studies of the LLVM and gcc compilers on ARM and x86, respectively; and the register windows of the SPARC. After a brief consideration of in-line expansion, we turned in Section 9.3 to the subject of parameters. We first considered parameter-passing modes, all of which are implemented by passing values, references, or closures. We noted that the goals of semantic clarity and implementation speed sometimes conflict: it is usually most efficient to pass a large parameter by reference, but the aliasing that results can lead to program bugs. In Section 9.3.3 we considered special parameter-passing mechanisms, including default (optional) parameters, named parameters, and variable-length parameter lists.
在最后三个主要部分中,我们讨论了异常处理机制,它允许程序以结构良好的方式从嵌套的子程序调用序列中“解开”;协程,它允许程序维护(并在两个或多个执行上下文之间切换);以及事件,它允许程序响应异步外部活动。在配套网站上,我们解释了如何使用协程进行离散事件模拟。我们还注意到它们可用于实现迭代器,但这里有更简单的替代方案。在第 13 章中,我们将基于协程来实现线程,这些线程彼此并行运行(或看起来并行运行)。
In the final three major sections we considered exception-handling mechanisms, which allow a program to “unwind” in a well-structured way from a nested sequence of subroutine calls; coroutines, which allow a program to maintain (and switch between) two or more execution contexts; and events, which allow a program to respond to asynchronous external activity. On the companion site we explained how coroutines are used for discrete event simulation. We also noted that they could be used to implement iterators, but here simpler alternatives exist. In Chapter 13, we will build on coroutines to implement threads, which run (or appear to run) in parallel with one another.
在几种情况下,我们可以看出,关于语言应该提供哪些类型的控制抽象,人们的共识正在不断演变。Fortran 和 Algol 60 等语言的有限参数传递模式已被更广泛或更灵活的选项所取代。几种语言使用默认和命名参数增强了参数的标准位置表示法。结构化程度较低的错误处理机制(如标签参数、非本地goto和动态绑定处理程序)已被结构化异常处理程序所取代,这些异常处理程序在子例程中具有词法作用域,在常见(无异常)情况下可以以零成本实现。传统信号处理机制的自发子例程调用已被专用线程中的回调所取代。在许多情况下,实现这些新功能需要编译器和运行时系统变得更加复杂。有时,如按名称调用参数、标签参数或非本地goto的情况,语义上令人困惑的功能也难以实现,放弃它们使编译器变得更简单。在其他情况下,有用但难以实现的语言特性继续出现在某些语言中,但在其他语言中却没有出现。此类示例包括一级子程序、协同程序、迭代器、延续和具有无限范围的本地对象。
In several cases we can discern an evolving consensus about the sorts of control abstractions that a language should provide. The limited parameter-passing modes of languages like Fortran and Algol 60 have been replaced by more extensive or flexible options. Several languages augment the standard positional notation for arguments with default and named parameters. Less-structured error-handling mechanisms, such as label parameters, nonlocal gotos, and dynamically bound handlers, have been replaced by structured exception handlers that are lexically scoped within subroutines, and can be implemented at zero cost in the common (no-exception) case. The spontaneous subroutine call of traditional signal-handling mechanisms have been replaced by callbacks in a dedicated thread. In many cases, implementing these newer features has required that compilers and run-time systems become more complex. Occasionally, as in the case of call-by-name parameters, label parameters, or nonlocal gotos, features that were semantically confusing were also difficult to implement, and abandoning them has made compilers simpler. In yet other cases language features that are useful but difficult to implement continue to appear in some languages but not in others. Examples in this category include first-class subroutines, coroutines, iterators, continuations, and local objects with unlimited extent.
9.1 尽可能多地描述命令式编程语言中的函数与数学中的函数的不同之处。
9.1 Describe as many ways as you can in which functions in imperative programming languages differ from functions in mathematics.
9.2 考虑以下 C++ 代码:class string_map { string cached_key; string cached_val; const string complex_lookup(const string key); // body specified anywhere public: const string operator[](const string key) { if (key == cached_key) return cached_val; string rtn_val = complex_lookup(key); cached_key = key; cached_val = rtn_val; return rtn_val; } };假设string_map::operator []包含程序中对complex_lookup的唯一调用。解释为什么程序员以文本方式内联扩展该调用并消除单独的函数是不明智的。
9.2 Consider the following code in C++:
class string_map {
string cached_key;
string cached_val;
const string complex_lookup(const string key);
// body specified elsewhere
public:
const string operator[](const string key) {
if (key == cached_key) return cached_val;
string rtn_val = complex_lookup(key);
cached_key = key;
cached_val = rtn_val;
return rtn_val;
}
};
Suppose that string_map::operator [] contains the only call to complex_lookup anywhere in the program. Explain why it would be unwise for the programmer to expand that call textually in-line and eliminate the separate function.
9.3 使用您最喜欢的语言和编译器,编写一个程序,可以告知某些子程序参数的评估顺序。
9.3 Using your favorite language and compiler, write a program that can tell the order in which certain subroutine parameters are evaluated.
9.4 考虑以下 C 语言中的(错误)程序:void foo() { int i; printf(“%d “, i++); } int main() { int j; for (j = 1; j <= 10; j++) foo(); }子程序foo中的局部变量i从未初始化。然而,在许多系统上,该程序将显示可重复的行为,打印0 1 2 3 4 5 6 7 8 9。请提出解释。同时解释为什么其他系统上的行为可能不同或不确定。
9.4 Consider the following (erroneous) program in C:
void foo() {
int i;
printf(“%d “, i++);
}
int main() {
int j;
for (j = 1; j <= 10; j++) foo();
}
Local variable i in subroutine foo is never initialized. On many systems, however, the program will display repeatable behavior, printing 0 1 2 3 4 5 6 7 8 9. Suggest an explanation. Also explain why the behavior on other systems might be different, or nondeterministic.
9.5 1980 年 Digital VAX 指令 集的标准调用序列不仅使用堆栈指针 ( sp ) 和帧指针 ( fp ),还使用单独的参数指针( ap )。在什么情况下,这个单独的指针会很有用?换句话说,什么时候不必将参数放置在距 fp 的静态已知偏移量处会很方便?
9.5 The standard calling sequence for the c. 1980 Digital VAX instruction set employed not only a stack pointer (sp) and frame pointer (fp), but a separate arguments pointer (ap) as well. Under what circumstances might this separate pointer be useful? In other words, when might it be handy not to have to place arguments at statically known offsets from the fp?
9.6 编写(用您选择的语言)一个过程或函数,它将具有四种不同的效果,具体取决于参数是通过值、通过引用、通过值/结果还是通过名称传递。
9.6 Write (in the language of your choice) a procedure or function that will have four different effects, depending on whether arguments are passed by value, by reference, by value/result, or by name.
9.7 考虑在 Fortran 中传递给子程序的表达式a + b。将此表达式作为对未命名临时变量的引用传递(如 Fortran 所做的那样)与按值传递(例如在 Pascal 中)之间是否存在语义上有意义的区别?也就是说,程序员能否分辨出作为值的参数和作为对临时变量的引用的参数之间的区别?
9.7 Consider an expression like a + b that is passed to a subroutine in Fortran. Is there any semantically meaningful difference between passing this expression as a reference to an unnamed temporary (as Fortran does) or passing it by value (as one might, for example, in Pascal)? That is, can the programmer tell the difference between a parameter that is a value and a parameter that is a reference to a temporary?
9.8 考虑 Fortran 77 中的以下子程序:subroutine shift(a, b, c) integer a, b, c a = b b = c end假设我们想调用shift(x, y, 0)但不想改变y的值。由于知道构建的表达式是作为临时变量传递的,我们决定调用shift(x, y+0, 0)。我们的代码一开始运行良好,但是当我们启用优化时(使用某些编译器)会失败。这是怎么回事?我们可以做什么呢?
9.8 Consider the following subroutine in Fortran 77:
subroutine shift(a, b, c)
integer a, b, c
a = b
b = c
end
Suppose we want to call shift(x, y, 0) but we don't want to change the value of y. Knowing that built-up expressions are passed as temporaries, we decide to call shift(x, y+0, 0). Our code works fine at first, but then (with some compilers) fails when we enable optimization. What is going on? What might we do instead?
9.9 在 Fortran IV 的某些实现中,以下代码将打印 3。您能给出解释吗?您认为较新的 Fortran 实现如何解决这个问题?c主程序调用 foo(2) print*, 2 stop end subroutine foo(x) x = x + 1 return end
9.9 In some implementations of Fortran IV, the following code would print a 3. Can you suggest an explanation? How do you suppose more recent Fortran implementations get around the problem?
c main program
call foo(2)
print*, 2
stop
end
subroutine foo(x)
x = x + 1
return
end
9.10 假设你正在编写一个程序,其中所有参数都必须按名称传递。你能编写一个子程序来交换其实际参数的值吗?请解释。(提示:考虑相互依赖的参数,如i和A[i]。)
9.10 Suppose you are writing a program in which all parameters must be passed by name. Can you write a subroutine that will swap the values of its actual parameters? Explain. (Hint: Consider mutually dependent parameters like i and A[i].)
9.11 你能用Java 或其他任何只带有共享调用参数的语言编写swap例程吗?在这样的语言中swap到底应 该做什么?(提示:考虑变量引用的对象与该对象的值 [内容] 之间的区别。)
9.11 Can you write a swap routine in Java, or in any other language with only call-by-sharing parameters? What exactly should swap do in such a language? (Hint: Think about the distinction between the object to which a variable refers and the value [contents] of that object.)
9.12如 第 9.3.1 节所述, Ada 83 中的输出参数可由调用方写入但不能读取。在 Ada 95 中,它们既可读取又可写入,但它们的生命周期开始时未初始化。您认为 Ada 95 的设计者为什么要进行这种改变?这样做有什么缺点吗?
9.12 As noted in Section 9.3.1, out parameters in Ada 83 can be written by the callee but not read. In Ada 95 they can be both read and written, but they begin their life uninitialized. Why do you think the designers of Ada 95 made this change? Does it have any drawbacks?
9.13 Swift 借鉴了 Ada 的思路,提供了一种inout参数模式。语言手册没有指定inout参数是通过引用传递还是通过值结果传递。编写一个程序来确定本地 Swift 编译器使用的实现。
9.13 Taking a cue from Ada, Swift provides an inout parameter mode. The language manual does not specify whether inout parameters are to be passed by reference or value-result. Write a program that determines the implementation used by your local Swift compiler.
9.14在 Pascal 中, 压缩记录的字段(示例 8.8)不能通过引用传递。同样,在通过引用传递子范围变量时,Pascal 要求相应形式参数的所有可能值对该子范围都有效:type small = 1..100; R = record x, y : small; end; S = packed record x, y : small; end; var a : 1..10; b : 1..1000; c : R; d : S; procedure foo(var n : small); begin n := 100; writeln(a); end; … a := 2; foo(b); (* ok *) foo(a); (* 静态语义错误 *) foo(cx); (* ok *) foo(dx); (* 静态语义错误 *)
利用你所学到的参数传递模式,解释这些语言限制。
9.14 Fields of packed records (Example 8.8) cannot be passed by reference in Pascal. Likewise, when passing a subrange variable by reference, Pascal requires that all possible values of the corresponding formal parameter be valid for the subrange:
type small = 1..100;
R = record x, y : small; end;
S = packed record x, y : small; end;
var a : 1..10;
b : 1..1000;
c : R;
d : S;
procedure foo(var n : small);
begin
n := 100;
writeln(a);
end;
…
a := 2;
foo(b); (* ok *)
foo(a); (* static semantic error *)
foo(c.x); (* ok *)
foo(d.x); (* static semantic error *)
Using what you have learned about parameter-passing modes, explain these language restrictions.
9.15 考虑 C 语言中的以下声明:double(*foo(double (*)(double, double[]), double)) (double, …);用英语描述foo的类型。
9.15 Consider the following declaration in C:
double(*foo(double (*)(double, double[]), double)) (double, …);
Describe in English the type of foo.
9.16 当程序员在子程序调用中省略可选参数时,程序运行速度会更快吗?为什么或为什么不?
9.16 Does a program run faster when the programmer leaves optional parameters out of a subroutine call? Why or why not?
9.17 你认为为什么高级编程语言很少支持可变长度参数列表?
9.17 Why do you suppose that variable-length argument lists are so seldom supported by high-level programming languages?
9.18在 练习 6.35的基础上,说明如何在 Scheme 中使用call-with-current-continuation实现异常。以Common Lisp 的handler-case为范本,建立语法模型。与练习 6.35一样,您可能需要define-syntax和dynamic-wind。
9.18 Building on Exercise 6.35, show how to implement exceptions using call-with-current-continuation in Scheme. Model your syntax after the handler-case of Common Lisp. As in Exercise 6.35, you will probably need define-syntax and dynamic-wind.
9.19 鉴于你所学到的关于结构化异常实现的知识,描述如何实现 Pascal 的非局部goto或 Algol 60 的标签参数(第 6.2 节)。你是否需要对这些功能的使用方式进行任何限制?
9.19 Given what you have learned about the implementation of structured exceptions, describe how you might implement the nonlocal gotos of Pascal or the label parameters of Algol 60 (Section 6.2). Do you need to place any restrictions on how these features can be used?
9.20描述 C++ 析构函数或 Java try…finally块的合理实现。编译器必须在程序的什么位置生成哪些代码,以确保在离开作用域时始终进行清理?
9.20 Describe a plausible implementation of C++ destructors or Java try… finally blocks. What code must the compiler generate, at what points in the program, to ensure that cleanup always occurs when leaving a scope?
9.21 使用线程在 Java 中构建对真正迭代器的支持。尝试将尽可能多的实现隐藏在合理的接口后面。特别是,隐藏名为yield的例程(由迭代器调用)和标准 Java Iterator接口例程(在循环体中调用)的实现中对new thread、thread.start、thread.join、wait和notify的任何使用。将迭代器的性能与内置迭代器对象的性能进行比较(结果可能不太好)。讨论您在语言的抽象功能中遇到的任何弱点。
9.21 Use threads to build support for true iterators in Java. Try to hide as much of the implementation as possible behind a reasonable interface. In particular, hide any uses of new thread, thread.start, thread.join, wait, and notify inside implementations of routines named yield (to be called by an iterator) and in the standard Java Iterator interface routines (to be called in the body of a loop). Compare the performance of your iterators to that of the built-in iterator objects (it probably won't be good). Discuss any weaknesses you encounter in the abstraction facilities of the language.
9.22 在 Common Lisp 中,多级返回使用catch和throw;大多数其他现代语言风格的异常处理使用handler-case和error。说明它们之间的区别主要在于风格,而不是表达能力。换句话说,说明每种功能都可以用来模拟另一种功能。
9.22 In Common Lisp, multilevel returns use catch and throw; exception handling in the style of most other modern languages uses handler-case and error. Show that the distinction between these is mainly a matter of style, rather than expressive power. In other words, show that each facility can be used to emulate the other.
9.23 编译并运行图 9.6中的程序。解释其行为。创建一个行为更可预测的新版本。
9.23 Compile and run the program in Figure 9.6. Explain its behavior. Create a new version that behaves more predictably.
9.24 使用 C#、Java 或其他具有基于线程的事件处理的语言,围绕示例 9.51 – 9.54的“暂停按钮”构建一个简单的程序。您的程序应打开一个小窗口,其中包含一个文本字段和两个按钮,一个标记为“暂停”,另一个标记为“恢复”。然后它应在文本字段中显示一个整数,从零开始,每秒计数一次。如果按下暂停按钮,计数应暂停;如果按下恢复按钮,计数应继续。
注意,您的程序至少需要两个线程 - 一个用于计数,一个用于处理事件。在 Java 中,JavaFX 包将自动创建处理程序线程,然后您的主程序可以进行计数。在 C# 中,某个现有线程需要调用Application.Run才能成为处理程序线程。在这种情况下,您需要第二个线程来进行计数。
9.24 In C#, Java, or some other language with thread-based event handling, build a simple program around the “pause button” of Examples 9.51–9.54. Your program should open a small window containing a text field and two buttons, one labeled “pause”, the other labeled “resume”. It should then display an integer in the text field, starting with zero and counting up once per second. If the pause button is pressed, the count should suspend; if the resume button is pressed, it should continue.
Note that your program will need at least two threads—one to do the counting, one to handle events. In Java, the JavaFX package will create the handler thread automatically, and your main program can do the counting. In C#, some existing thread will need to call Application.Run in order to become a handler thread. In this case you'll need a second thread to do the counting.
9.25 通过添加“克隆”按钮来扩展您对上一个问题的答案。按下此按钮应创建一个包含另一个计数器的附加窗口。当然,这将需要额外的线程。
9.25 Extend your answer to the previous problem by adding a “clone” button. Pushing this button should create an additional window containing another counter. This will, of course, require additional threads.
9.26–9.36 更深入。
9.26–9.36 In More Depth.
9.37 探索 GNU Ada 翻译器gnat中子程序调用的细节。特别注意更复杂的语言特性,包括嵌套块中的声明(第 3.3.2 节)、动态大小数组(第 8.2.2 节)、输入/输出参数(第 9.3.1 节)、可选和命名参数(第 9.3.3 节)、通用子程序(第 7.3.1 节)、异常(第 9.4 节)和并发(“启动时细化”,第 13.2.3 节)。
9.37 Explore the details of subroutine calls in the GNU Ada translator gnat. Pay particular attention to the more complex language features, including declarations in nested blocks (Section 3.3.2), dynamic-size arrays (Section 8.2.2), in/out parameters (Section 9.3.1), optional and named parameters (Section 9.3.3), generic subroutines (Section 7.3.1), exceptions (Section 9.4), and concurrency (“Launch-at-Elaboration,” Section 13.2.3).
9.38 如果你正在设计一种新的命令式语言,你会选择哪组参数模式?为什么?
9.38 If you were designing a new imperative language, what set of parameter modes would you pick? Why?
9.39 了解 PHP 中的引用和引用赋值运算符。讨论它们与 C++ 引用的相同点和不同点。特别要注意的是,PHP 中的赋值可以改变引用变量所引用的对象。为什么 PHP 允许这样做而 C++ 不允许?
9.39 Learn about references and the reference assignment operator in PHP. Discuss the similarities and differences between these and the references of C++. In particular, note that assignments in PHP can change the object to which a reference variable refers. Why does PHP allow this but C++ does not?
9.40 了解 C++ 中的方法指针。它们有什么用处?它们与封装方法的 C# 委托有何不同?
9.40 Learn about pointers to methods in C++. What are they useful for? How do they differ from a C# delegate that encapsulates a method?
9.41 查找几种有异常的语言的手册,并查找预定义异常集(即语言实现可能自动引发的异常集)。讨论不同语言定义的异常集之间的差异。如果你正在设计一个异常处理工具,你会预定义哪些异常(如果有的话)?为什么?
9.41 Find manuals for several languages with exceptions and look up the set of predefined exceptions—those that may be raised automatically by the language implementation. Discuss the differences among the sets defined by different languages. If you were designing an exception-handling facility, what exceptions, if any, would you make predefined? Why?
9.42 Eiffel 是“替代模型”异常处理的一个例外。其救援子句表面上类似于catch块,但它必须重试它所附加的例程,或者允许异常沿调用链向上传播。换句话说,当控制权从救援子句的末尾脱落时,默认行为是重新引发异常。阅读“契约式设计”,这是此异常处理机制支持的编程方法。您是否同意反对替代的论点?解释一下。
9.42 Eiffel is an exception to the “replacement model” of exception handling. Its rescue clause is superficially similar to a catch block, but it must either retry the routine to which it is attached or allow the exception to propagate up the call chain. Put another way, the default behavior when control falls off the end of the rescue clause is to reraise the exception. Read up on “Design by Contract,” the programming methodology supported by this exception-handling mechanism. Do you agree or disagree with the argument against replacement? Explain.
9.43 学习 Common Lisp 中非本地控制传输的细节。编写一个教程来解释tagbody和go;block和return-from;catch和throw;以及restart-case、restart-bind、handler-case、handler-bind、find-restart、invoke-restart、ignore-errors、signal和error。你觉得所有这些机制怎么样?是不是有点矫枉过正?一定要给出一个例子来说明handler-bind的用法。
9.43 Learn the details of nonlocal control transfer in Common Lisp. Write a tutorial that explains tagbody and go; block and return-from; catch and throw; and restart-case, restart-bind, handler-case, handler-bind, find-restart, invoke-restart, ignore-errors, signal, and error. What do you think of all this machinery? Is it over-kill? Be sure to give an example that illustrates the use of handler-bind.
9.44对于 Common Lisp、Modula-3 和 Java,比较 unwind-protect和try…finally的语义。具体来说,如果在 cleanup 子句中出现异常会发生什么?
9.44 For Common Lisp, Modula-3, and Java, compare the semantics of unwind-protect and try…finally. Specifically, what happens if an exception arises within a cleanup clause?
9.45如 第 9.6.2 节末尾所述,事件处理程序需要快速执行或将其工作传递给另一个线程。在async和await原语中可以找到后者的一个特别优雅的机制C# 5 和 F# 中类似的async和let!。阅读这些promitives支持的异步编程模型。解释它们与迭代器的(实现级)连接(第 C-9.5.3 节)。编写一个基于 GUI 的程序或网络服务器,充分利用它们。
9.45 As noted near the end of Section 9.6.2, an event-handler needs either to execute quickly or to pass its work off to another thread. A particularly elegant mechanism for the latter can be found in the async and await primitives of C# 5 and the similar async and let! of F#. Read up on the asynchronous programming model supported by these promitives. Explain their (implementation-level) connection to iterators (Section C-9.5.3). Write a GUI-based program or a network server that makes good use of them.
9.46 比较和对比几种 GUI 系统的事件处理机制。处理程序如何与事件绑定?你能控制它们的调用顺序吗?每个系统支持多少个事件处理线程?处理程序线程是如何以及何时创建的?它们如何与程序的其余部分同步?
9.46 Compare and contrast the event-handling mechanisms of several GUI systems. How are handlers bound to events? Can you control the order in which they are invoked? How many event-handling threads does each system support? How and when are handler threads created? How do they synchronize with the rest of the program?
9.47–9.52 更深入。
9.47–9.52 In More Depth.
递归子程序最初通过 McCarthy 在 Lisp 上的工作而为人所知 [ McC60 ]。7递归子程序的基于堆栈的空间管理是使用 Algol 60 编译器开发的(例如,参见 Randell 和 Russell [ RR64 ])。(由于范围问题,Lisp 中的子程序空间需要更通用的基于堆的分配。)Dijkstra [ Dij60 ] 介绍了使用显示访问非本地数据的早期讨论。Hanson [ Han81 ] 认为嵌套子程序是不必要的。
Recursive subroutines became known primarily through McCarthy's work on Lisp [McC60].7 Stack-based space management for recursive subroutines developed with compilers for Algol 60 (see, e.g., Randell and Russell [RR64]). (Because of issues of extent, subroutine space in Lisp requires more general, heap-based allocation.) Dijkstra [Dij60] presents an early discussion of the use of displays to access nonlocal data. Hanson [Han81] argues that nested subroutines are unnecessary.
gcc的调用序列和堆栈约定部分记录在随编译器分发的texinfo文件中(请参阅www.gnu.org/software/gcc)。LLVM 的文档可在 llvm.org 找到。配套站点上描述的几个细节是通过检查两个编译器的输出“逆向工程”的。
Calling sequences and stack conventions for gcc are partially documented in the texinfo files distributed with the compiler (see www.gnu.org/software/gcc). Documentation for LLVM can be found at llvm.org. Several of the details described on the companion site were “reverse engineered” by examining the output of the two compilers.
Ada 语言基本原理 [ IBFW91,第 8 章] 包含对参数传递模式的出色讨论。Harbison [ Har92,第 6.2-6.3 节]描述了 Modula-3 模式并将其与其他语言的模式进行了比较。Liskov 和 Guttag [ LG86,第 25 页] 将 Clu 中的共享调用比作 Lisp 中的参数传递。按名称调用参数源于 Alonzo Church [ Chu41 ] 的 lambda 演算,我们将在 C-11.7.1 节中对其进行更详细的介绍。Thunk 最早由 Ingerman [ Ing61 ]描述。Fleck [ Fle76 ] 讨论了尝试编写带有按名称调用参数的交换例程时遇到的问题(练习 9.10)。
The Ada language rationale [IBFW91, Chap. 8] contains an excellent discussion of parameter-passing modes. Harbison [Har92, Secs. 6.2–6.3] describes the Modula-3 modes and compares them to those of other languages. Liskov and Guttag [LG86, p. 25] liken call-by-sharing in Clu to parameter passing in Lisp. Call-by-name parameters have their roots in the lambda calculus of Alonzo Church [Chu41], which we consider in more detail in Section C-11.7.1. Thunks were first described by Ingerman [Ing61]. Fleck [Fle76] discusses the problems involved in trying to write a swap routine with call-by-name parameters (Exercise 9.10).
MacLaren [ Mac77 ] 描述了 PL/I 中的异常处理。Ada 和大多数较新的语言的词法作用域替代方案大量借鉴了 Goodenough [ Goo75 ]的工作。Luckam 和 Polak [ LP80 ]正式描述了 Ada 的语义。Clu 的异常是一个有趣的历史先驱;详细信息可以在 Liskov 和 Snyder 的著作 [ LS79 ] 中找到。Meyer [ Mey92a ] 讨论了 Eiffel 中的契约式设计和异常处理。Friedman、Wand 和 Haynes [ FWH01,第 8-9 章] 对 Scheme 中的延续传递风格进行了出色的解释。
MacLaren [Mac77] describes exception handling in PL/I. The lexically scoped alternative of Ada, and of most more recent languages, draws heavily on the work of Goodenough [Goo75]. Ada's semantics are described formally by Luckam and Polak [LP80]. Clu's exceptions are an interesting historical precursor; details can be found in the work of Liskov and Snyder [LS79]. Meyer [Mey92a] discusses Design by Contract and exception handling in Eiffel. Friedman, Wand, and Haynes [FWH01, Chaps. 8-9] provide an excellent explanation of continuation-passing style in Scheme.
Conway [ Con63 ]的作品中出现了对协程的早期描述,他用协程来表示编译的各个阶段。Birtwistle 等人 [ BDMN73 ] 在 Simula 67 中提供了使用协程进行模拟的教程介绍。Cactus 堆栈至少可以追溯到 20 世纪 60 年代中期;Burroughs B6500 和 B7500 计算机 [ HD68 ] 在硬件上直接支持它们。Murer 等人 [ MOSS96 ] 讨论了 Sather 编程语言(Eiffel 的后代)中迭代器的实现。Von Behren 等人 [ vCZ + 03 ] 描述了一个基于块的堆栈分配系统。
An early description of coroutines appears in the work of Conway [Con63], who used them to represent the phases of compilation. Birtwistle et al. [BDMN73] provide a tutorial introduction to the use of coroutines for simulation in Simula 67. Cactus stacks date from at least the mid-1960s; they were supported directly in hardware by the Burroughs B6500 and B7500 computers [HD68]. Murer et al. [MOSS96] discuss the implementation of iterators in the Sather programming language (a descendant of Eiffel). Von Behren et al. [vCZ+03] describe a system with chunk-based stack allocation.
在 第 3 章中 我们介绍了数据抽象发展的几个阶段,重点介绍了控制名称可见性的作用域机制。我们从全局变量开始,其生存期跨越程序执行。然后,我们添加了局部变量,其生存期仅限于单个子例程的执行;嵌套作用域,允许子例程本身是局部的;以及静态变量,其生存期跨越执行,但其名称仅在单个作用域内可见。接下来是模块,允许一组子例程共享一组静态变量;模块类型,允许程序员实例化给定抽象的多个实例,以及类,允许程序员定义相关抽象的系列。
In Chapter 3 we presented several stages in the development of data abstraction, with an emphasis on the scoping mechanisms that control the visibility of names. We began with global variables, whose lifetime spans program execution. We then added local variables, whose lifetime is limited to the execution of a single subroutine; nested scopes, which allow subroutines themselves to be local; and static variables, whose lifetime spans execution, but whose names are visible only within a single scope. These were followed by modules, which allow a collection of subroutines to share a set of static variables; module types, which allow the programmer to instantiate multiple instances of a given abstraction, and classes, which allow the programmer to define families of related abstractions.
普通模块鼓励“管理器”风格的编程,其中模块导出抽象类型。模块类型和类允许模块本身成为抽象类型。区别在两个方面显而易见。首先,通常从管理器模块导出的显式创建和销毁例程被模块类型实例的创建和销毁所取代。其次,调用特定模块实例中的例程取代了调用需要导出类型的变量作为参数的通用例程。类在模块即类型方法的基础上添加了继承机制,允许将新抽象定义为对现有抽象的改进或扩展,以及动态方法绑定,允许抽象的新版本显示新改进的行为,即使在需要早期版本的上下文中使用也是如此。类的实例称为对象;基于类的语言和编程技术被称为面向对象的。1
Ordinary modules encourage a “manager” style of programming, in which a module exports an abstract type. Module types and classes allow the module itself to be the abstract type. The distinction becomes apparent in two ways. First, the explicit create and destroy routines typically exported from a manager module are replaced by creation and destruction of an instance of the module type. Second, invocation of a routine in a particular module instance replaces invocation of a general routine that expects a variable of the exported type as argument. Classes build on the module-as-type approach by adding mechanisms for inheritance, which allows new abstractions to be defined as refinements or extensions to existing ones, and dynamic method binding, which allows a new version of an abstraction to display newly refined behavior, even when used in a context that expects an earlier version. An instance of a class is known as an object; languages and programming techniques based on classes are said to be object-oriented.1
第 3 章中介绍的数据抽象机制的逐步演化是一种组织思想的有用方法,但它并不能完全反映语言特性的历史发展。特别是,认为面向对象编程是模块的产物是不准确的。相反,面向对象编程的所有三个基本概念(封装、继承和动态方法绑定)都源于 Simula 编程语言,该语言由挪威计算中心的 Ole-Johan Dahl 和 Kristen Nygaard 于 20 世纪 60 年代中期开发。2与现代面向对象语言相比,Simula 在封装的数据隐藏部分较弱,而 Clu、Modula、Euclid 和相关语言在 20 世纪 70 年代正是在这一领域做出了重要贡献。与此同时,继承和动态方法绑定的思想在 20 世纪 70 年代被 Smalltalk 采纳和完善。
The stepwise evolution of data abstraction mechanisms presented in Chapter 3 is a useful way to organize ideas, but it does not completely reflect the historical development of language features. In particular, it would be inaccurate to suggest that object-oriented programming developed as an outgrowth of modules. Rather, all three of the fundamental concepts of object-oriented programming—encapsulation, inheritance, and dynamic method binding—have their roots in the Simula programming language, developed in the mid-1960s by Ole-Johan Dahl and Kristen Nygaard of the Norwegian Computing Center.2 In comparison to modern object-oriented languages, Simula was weak in the data hiding part of encapsulation, and it was in this area that Clu, Modula, Euclid, and related languages made important contributions in the 1970s. At the same time, the ideas of inheritance and dynamic method binding were adopted and refined in Smalltalk over the course of the 1970s.
Smalltalk 采用独特的“基于消息”的编程模型,具有动态类型和不寻常的术语和语法。动态类型往往会使实现相对较慢,并延迟错误报告。该语言还紧密集成到图形编程环境中,因此很难跨系统移植。由于这些原因,考虑到 Smalltalk 对后续开发的影响,其使用范围没有人们想象的那么广泛。Eiffel、C++、Ada 95、Fortran 2003、Java 和 C# 等语言在很大程度上代表了 Smalltalk 的继承和动态方法绑定与“主流”命令式语法和语义的重新整合。另一方面,Objective-C 以相对纯粹和纯粹的形式将 Smalltalk 风格的消息传递和动态类型与用于对象内操作的传统 C 语法相结合。面向对象在函数式语言中也变得很重要,例如 Common Lisp 对象系统 (CLOS [ Kee89;Ste90,第 28 章]) 和 OCaml 的对象。
Smalltalk employed a distinctive “message-based” programming model, with dynamic typing and unusual terminology and syntax. The dynamic typing tended to make implementations relatively slow, and delayed the reporting of errors. The language was also tightly integrated into a graphical programming environment, making it difficult to port across systems. For these reasons, Smalltalk was less widely used than one might have expected, given the influence it had on subsequent developments. Languages like Eiffel, C++, Ada 95, Fortran 2003, Java, and C# represented to a large extent a reintegration of the inheritance and dynamic method binding of Smalltalk with “mainstream” imperative syntax and semantics. In an alternative vein, Objective-C combined Smalltalk-style messaging and dynamic typing, in a relatively pure and unadulterated form, with traditional C syntax for intra-object operations. Object orientation has also become important in functional languages, as exemplified by the Common Lisp Object System (CLOS [Kee89; Ste90, Chap. 28]) and the objects of OCaml.
最近,动态类型对象在 Python 和 Ruby 等语言中获得了新的流行度,而静态类型对象则继续出现在 Scala 和 Go 等语言中。Swift 是 Objective-C 的后继者,它遵循其前身(实际上也是 OCaml)的模式,将动态类型对象分层放在静态类型语言之上。
More recently, dynamically typed objects have gained new popularity in languages like Python and Ruby, while statically typed objects continue to appear in languages like Scala and Go. Swift, the successor to Objective-C, follows the pattern of its predecessor (and of OCaml, in fact) in layering dynamically typed objects on top of an otherwise statically typed language.
在10.1 节中,我们概述了面向对象编程及其三个基本概念。在10.2 节中,我们将更详细地讨论封装和数据隐藏。然后,我们在10.3 节中讨论对象初始化和终止化,在10.4 节中讨论动态方法绑定。在10.6 节(主要在配套站点上)中,我们讨论多重继承的主题,其中一个类是根据多个现有类定义的。我们将看到,多重继承带来了一些特别棘手的语义和实现挑战。最后,在第 10.7 节中,我们重新审视面向对象的定义,考虑一种语言能够或应该在多大程度上建模一切都是对象。我们的讨论主要集中在 Smalltalk、Eiffel、C++ 和 Java 上,尽管我们也将有机会提到许多其他语言。我们将在第14.4.4 节中回到动态类型对象的主题。
In Section 10.1 we provide an overview of object-oriented programming and of its three fundamental concepts. We consider encapsulation and data hiding in more detail in Section 10.2. We then consider object initialization and finalizationin Section 10.3, and dynamic method binding in Section 10.4. In Section 10.6 (mostly on the companion site) we consider the subject of multiple inheritance, in which a class is defined in terms of more than one existing class. As we shall see, multiple inheritance introduces some particularly thorny semantic and implementation challenges. Finally, in Section 10.7, we revisit the definition of object orientation, considering the extent to which a language can or should model everything as an object. Most of our discussion will focus on Smalltalk, Eiffel, C++, and Java, though we shall have occasion to mention many other languages as well. We will return to the subject of dynamically typed objects in Section 14.4.4.
公众 list_node声明中的标签将抽象实现所需的成员与抽象用户可用的成员分开。 按照3.3.4 节的术语,出现在public标签之后的成员将从类中导出;而出现在标签之前的成员则不会导出。 C++ 还提供了private标签,因此可以根据需要首先列出类的公开可见部分(甚至混合列出)。 在许多其他语言中,公共数据和子例程成员(字段和方法)必须单独标记(有关更多信息,请参见10.2.2 节)。 请注意,C++ 类是开放作用域,如3.3.4 节所定义;不需要显式导入任何内容。
The public label within the declaration of list_node separates members required by the implementation of the abstraction from members available to users of the abstraction. In the terminology of Section 3.3.4, members that appear after the public label are exported from the class; members that appear before the label are not. C++ also provides a private label, so the publicly visible portions of a class can be listed first if desired (or even intermixed). In many other languages, public data and subroutine members (fields and methods) must be individually so labeled (more on this in Section 10.2.2). Note that C++ classes are open scopes, as defined in Section 3.3.4; nothing needs to be explicitly imported.
面向对象程序往往比普通命令式程序调用更多的子程序,而且子程序往往更短。在冯·诺依曼语言中,许多通过直接访问记录字段可以完成的事情往往隐藏在面向对象语言的对象方法中。事实上,许多程序员认为声明公共字段是一种不好的做法,因为这样做会让抽象的用户直接访问内部表示,并且使得在不更改用户代码的情况下无法更改该表示。可以说,我们应该将list_node的val字段设为私有,并使用get_val和set_val方法来读取和写入它。
Object-oriented programs tend to make many more subroutine calls than do ordinary imperative programs, and the subroutines tend to be shorter. Lots of things that would be accomplished by direct access to record fields in a von Neumann language tend to be hidden inside object methods in an object-oriented language. Many programmers in fact consider it bad style to declare public fields, because doing so gives users of an abstraction direct access to the internal representation, and makes it impossible to change that representation without changing the user code as well. Arguably, we should make the val field of list_node private, with get_val and set_val methods to read and write it.
在 C++ 中创建派生类的对象时,编译器会首先调用基类的构造函数,然后再调用派生类的构造函数。在我们的队列示例中,如果派生类没有构造函数,则仍会调用列表构造函数 — 这当然是我们想要的。我们将在第 10.3 节进一步讨论构造函数。
When an object of a derived class is created in C++, the compiler arranges to call the constructor for the base class first, and then to call the constructor of the derived class. In our queue example, where the derived class lacks a constructor, the list constructor will still be called—which is, of course, what we want. We will discuss constructors further in Section 10.3.
通过从旧类派生新类,程序员可以创建任意深度的类层次结构,并在树的每一层上添加附加功能。 Smalltalk 和 Java 的标准库分别有七层和八层之深。 (与 C++ 不同, Smalltalk 和 Java 都有一个根超类Object,所有其他类都从该超类派生而来。 C#、Objective-C 和 Eiffel 有一个类似的类; Eiffel 称之为ANY。)
By deriving new classes from old ones, the programmer can create arbitrarily deep class hierarchies, with additional functionality at every level of the tree. The standard libraries for Smalltalk and Java are as many as seven and eight levels deep, respectively. (Unlike C++, both Smalltalk and Java have a single root superclass, Object, from which all other classes are derived. C#, Objective-C, and Eiffel have a similar class; Eiffel calls it ANY.)
精明的读者可能已经注意到,我们的各种列表和队列都嵌入了这样的假设:每个列表节点中的项都是整数。实际上,我们希望能够拥有多种项的列表和队列,所有这些都基于大量代码的单一副本。在 Ruby 或 Python 等动态类型语言中,这是很自然的:val 字段没有静态类型,任何类型的对象都可以添加到列表和队列中,也可以从中删除。
The astute reader may have noticed that our various lists and queues have all embedded the assumption that the item in each list node is an integer. In practice, we should like to be able to have lists and queues of many kinds of items, all based on a single copy of the bulk of the code. In a dynamically typed language like Ruby or Python, this is natural: the val field would have no static type, and objects of any kind could be added to, and removed from, lists and queues.
简而言之,泛型是为了抽象不相关的类型而存在的,这是继承所不支持的。除了 C++,泛型还出现在大多数其他静态类型的面向对象语言中,包括 Eiffel、Java、C# 和 OCaml。
In a nutshell, generics exist for the purpose of abstracting over unrelated types, something that inheritance does not support. In addition to C++, generics appear in most other statically typed object-oriented languages, including Eiffel, Java, C#, and OCaml.
封装机制使程序员能够将数据和对其进行操作的子程序组合到一个地方,并向抽象的用户隐藏不相关的细节。在上一节(以及第 3.3.5 节)中,我们将面向对象编程视为 Simula 和 Euclid 的“模块即类型”机制的扩展。也可以将面向对象编程视为 Simula 和 Euclid 的“模块即类型”机制的扩展。在“模块作为管理器”框架中进行面向对象编程。在下面的第一小节中,我们将考虑非面向对象语言中模块的数据隐藏机制。在第二小节中,我们将考虑在向模块添加继承时出现的新数据隐藏问题。在第三小节中,我们将简要回顾模块作为管理器方法,并展示包括 Ada 95 和 Fortran 2003 在内的几种语言如何向记录添加继承,从而允许(静态)模块继续提供数据隐藏。
Encapsulation mechanisms enable the programmer to group data and the subroutines that operate on them together in one place, and to hide irrelevant details from the users of an abstraction. In the preceding section (and likewise Section 3.3.5) we cast object-oriented programming as an extension of the “module-as-type” mechanisms of Simula and Euclid. It is also possible to cast object-oriented programming in a “module-as-manager” framework. In the first subsection below we consider the data-hiding mechanisms of modules in non-object-oriented languages. In the second subsection we consider the new data-hiding issues that arise when we add inheritance to modules. In the third subsection we briefly return to the module-as-manager approach, and show how several languages, including Ada 95 and Fortran 2003, add inheritance to records, allowing (static) modules to continue to provide data hiding.
数据隐藏的范围规则是 Clu、Modula、Euclid 和 20 世纪 70 年代其他基于模块的语言的主要创新之一。在 Clu 和 Euclid 中,模块的声明和定义(头和主体)总是一起出现。在 Modula-2 中,程序员可以选择将头和主体放在单独的文件中。不幸的是,没有办法将头分为公共和私有部分;其中的所有内容都是公共的(即导出的)。数据隐藏的唯一让步是可以在头中声明指针类型,而无需透露它们指向的对象的结构。由于大多数机器上的指针大小相同,因此编译器可以为模块的用户生成代码(侧边栏 10.1),而无需隐藏信息。
Scope rules for data hiding were one of the principal innovations of Clu, Modula, Euclid, and other module-based languages of the 1970s. In Clu and Euclid, the declaration and definition (header and body) of a module always appeared together. In Modula-2, programmers had the option of placing the header and the body in separate files. Unfortunately, there was no way to divide the header into public and private parts; everything in it was public (i.e., exported). The only concession to data hiding was that pointer types could be declared in a header without revealing the structure of the objects to which they pointed. Compilers could generate code for the users of a module (Sidebar 10.1) without the hidden information, since pointers are all of equal size on most machines.
由于静态管理器式模块仅影响名称的可见性,因此不会引入特殊的代码生成问题。模块内部变量和其他数据的存储管理方式与模块外部数据的存储管理方式完全相同。如果模块出现在全局范围内,则其数据可以静态分配。如果模块出现在子程序中,那么当调用子程序时,其数据可以在堆栈上以已知偏移量分配,并在返回时回收。
Because they affect only the visibility of names, static, manager-style modules introduce no special code generation issues. Storage for variables and other data inside a module is managed in precisely the same way as storage for data immediately outside the module. If the module appears in a global scope, then its data can be allocated statically. If the module appears within a subroutine, then its data can be allocated on the stack, at known offsets, when the subroutine is called, and reclaimed when it returns.
模块类型,如 Euclid 和 ML 中的模块类型,稍微复杂一些:它们允许模块拥有任意数量的实例。显而易见的实现类似于记录的实现。如果模块中的所有数据都具有静态已知的大小,则可以为每个单独的数据分配模块存储中的静态偏移量。如果某些数据的大小直到运行时才知道,则可以将模块的存储分为固定大小和可变大小的部分,在固定大小部分的开头有一个内幕向量(描述符)。可以根据需要在堆栈或堆中静态分配模块的实例。
Module types, as in Euclid and ML, are somewhat more complicated: they allow a module to have an arbitrary number of instances. The obvious implementation then resembles that of a record. If all of the data in the module have a statically known size, then each individual datum can be assigned a static offset within the module's storage. If the size of some of the data is not known until run time, then the module's storage can be divided into fixed-size and variable-size portions, with a dope vector (descriptor) at the beginning of the fixed-size portion. Instances of the module can be allocated statically, on the stack, or in the heap, as appropriate.
如第 C-3.8 节所述,Java 包和 C/C++/C# 命名空间可以分布在多个编译单元(文件)中。在 C、C++ 和 C# 中,单个文件也可以包含多个命名空间的片段。更重要的是,许多现代语言(包括 Java 和 C#)都摒弃了单独的标头和主体的概念。虽然程序员仍然必须定义接口(并通过公共声明指定它),但无需手动识别出于实现原因需要在标头中的代码:相反,编译器负责从模块的全文中自动提取此信息。出于软件工程目的,可能仍然需要创建模块的初步“骨架”版本,以便可以针对该版本编译其他模块,但这是可选的。为了协助项目管理和文档编制,许多 Java 和 C# 实现都提供了一种工具,可以从模块的完整文本中提取用户所需的最少信息。
As noted in Section C-3.8, Java packages and C/C++/C# namespaces can be spread across multiple compilation units (files). In C, C++, and C#, a single file can also contain pieces of more than one namespace. More significantly, many modern languages, including Java and C#, dispense with the notion of separate headers and bodies. While the programmer must still define the interface (and specify it via public declarations), there is no need to manually identify code that needs to be in the header for implementation reasons: instead the compiler is responsible for extracting this information automatically from the full text of the module. For software engineering purposes it may still be desirable to create preliminary “skeleton” versions of a module, against which other modules can be compiled, but this is optional. To assist in project management and documentation, many Java and C# implementations provide a tool that will extract from the complete text of a module the minimum information required by its users.
随着继承的引入,面向对象语言必须补充基于模块的语言的范围规则以解决其他问题。例如,基类应该对其成员在派生类中的可见性进行多大程度的控制?基类的私有成员是否应该对派生类的方法可见?基类的公共成员是否应该始终是派生类的公共成员(即对派生类的用户可见)?
With the introduction of inheritance, object-oriented languages must supplement the scope rules of module-based languages to cover additional issues. For example, how much control should a base class exercise over the visibility of its members in derived classes? Should private members of a base class be visible to methods of a derived class? Should public members of a base class always be public members of a derived class (i.e., be visible to users of the derived class)?
除了public和private标签之外,C++ 还允许将类的成员指定为protected。 protected 成员仅对其自身类或从该类派生的类的方法可见。在我们的示例中,list的protected 成员M不仅可由list本身的方法访问,还可由queue的方法访问。但是,与 public 成员不同,M对list或queue对象的任意用户都是不可见的。
In addition to the public and private labels, C++ allows members of a class to be designated protected. A protected member is visible only to methods of its own class or of classes derived from that class. In our examples, a protected member M of list would be accessible not only to methods of list itself but also to methods of queue. Unlike public members, however, M would not be visible to arbitrary users of list or queue objects.
C++可见性规则背后的基本哲学可以概括如下:
The basic philosophy behind the visibility rules of C++ can be summarized as follows:
■ 任何类都可以限制其成员的可见性。公共成员在类声明范围内的任何地方都可见。私有成员仅在类的方法内可见。受保护的成员在类或其后代的方法内可见。(作为正常规则的例外,类可以指定某些其他朋友类或子例程应有权访问其私有成员。)
■ Any class can limit the visibility of its members. Public members are visible anywhere the class declaration is in scope. Private members are visible only inside the class's methods. Protected members are visible inside methods of the class or its descendants. (As an exception to the normal rules, a class can specify that certain other friend classes or subroutines should have access to its private members.)
■ 派生类可以限制基类成员的可见性,但永远不能增加其可见性。3基类的私有成员在派生类中永远不可见。类。公共基类的受保护成员和公共成员在派生类中分别是受保护成员或公共成员。受保护基类的受保护成员和公共成员是派生类的受保护成员。私有基类的受保护成员和公共成员是派生类的私有成员。
■ A derived class can restrict the visibility of members of a base class, but can never increase it.3 Private members of a base class are never visible in a derived class. Protected and public members of a public base class are protected or public, respectively, in a derived class. Protected and public members of a protected base class are protected members of a derived class. Protected and public members of a private base class are private members of a derived class.
■通过将基类声明 为 protected或private来限制基类成员可见性的派生类可以通过在派生类声明的protected或public部分插入using声明来恢复基类各个成员的可见性。
■ A derived class that limits the visibility of members of a base class by declaring that base class protected or private can restore the visibility of individual members of the base class by inserting a using declaration in the protected or public portion of the derived class declaration.
■派生类可以通过显式地 delete来使基类的方法(但不是字段)不可访问(对其他方法和其自身而言)。
■ A derived class can make methods (though not fields) of a base class inaccessible (to others and to itself) by explicitly delete-ing them.
其他面向对象语言对可见性采用不同的方法。Eiffel 在它可以支持的可见性模式方面比 C++ 更灵活,但它不遵守上述 C++ 原则中的第一条。Eiffel 中的派生类可以同时限制和增加基类成员的可见性。每个方法(在 Eiffel 中称为特性)都可以指定自己的导出状态。如果状态为{NONE},则成员实际上是私有的(在 Eiffel 中称为机密)。如果状态为{ANY},则成员实际上是公共的(在 Eiffel 中称为通常可用)。在一般情况下,状态可以是任意的类名列表,在这种情况下,该特性被认为仅对这些类及其后代有选择地可用。任何从基类继承的特性都可以在派生类中被赋予新状态。
Other object-oriented languages take different approaches to visibility. Eiffel is more flexible than C++ in the patterns of visibility it can support, but it does not adhere to the first of the C++ principles above. Derived classes in Eiffel can both restrict and increase the visibility of members of base classes. Every method (called a feature in Eiffel) can specify its own export status. If the status is {NONE} then the member is effectively private (called secret in Eiffel). If the status is {ANY} then the member is effectively public (called generally available in Eiffel). In the general case the status can be an arbitrary list of class names, in which case the feature is said to be selectively available to those classes and their descendants only. Any feature inherited from a base class can be given a new status in a derived class.
Java 和 C# 在声明public、protected和private成员时遵循 C++,但不提供对基类的protected和private指定;派生类既不能增加也不能限制基类成员的可见性。但是,它可以通过定义具有相同名称的新方法隐藏字段或覆盖方法;由于缺少范围解析运算符,新类的用户无法访问旧成员。在 Java 中,方法的覆盖版本不能比基类中的版本具有更严格的可见性限制。
Java and C# follow C++ in the declaration of public, protected, and private members, but do not provide the protected and private designations for base classes; a derived class can neither increase nor restrict the visibility of members of a base class. It can, however, hide a field or override a method by defining a new one with the same name; the lack of a scope resolution operator makes the old member inaccessible to users of the new class. In Java, the overriding version of a method cannot have more restrictive visibility than the version in the base class.
protected关键字在 Java 中的含义与在 C++ 中略有不同: Java 类的受保护成员不仅在派生类中可见,而且在声明该类的整个包(命名空间)中也可见。Java 中没有显式访问修饰符的类成员在声明该类的整个包中可见,但在其他包中的任何派生类中不可见。C# 定义protected与 C++ 一样,但提供了一个额外的内部关键字,使成员在类所在的整个程序集中可见。(程序集是链接在一起的编译单元的集合,相当于Java 中的.jar文件。)默认情况下,C# 类的成员是私有的。
The protected keyword has a slightly different meaning in Java than it does in C++: a protected member of a Java class is visible not only within derived classes but also within the entire package (namespace) in which the class is declared. A class member with no explicit access modifier in Java is visible throughout the package in which the class is declared, but not in any derived classes that reside in other packages. C# defines protected as C++ does, but provides an additional internal keyword that makes a member visible throughout the assembly in which the class appears. (An assembly is a collection of linked-together compilation units, comparable to a .jar file in Java.) Members of a C# class are private by default.
在 Smalltalk 和 Objective-C 中,成员可见性问题从未出现:语言允许代码在运行时尝试调用任何对象中的任何方法名称。如果方法存在(具有正确数量的参数),则调用继续;否则会导致运行时错误。在这些语言中,没有办法让某个方法对程序的某些部分可用,而对其他部分不可用。与此相关,Python 类成员始终是公共的。在 Ruby 中,字段始终是私有的;不仅如此,它们只能由其所属的单个对象的方法访问。
In Smalltalk and Objective-C, the issue of member visibility never arises: the language allows code at run time to attempt a call of any method name in any object. If the method exists (with the right number of parameters), then the invocation proceeds; otherwise a run-time error results. There is no way in these languages to make a method available to some parts of a program but not to others. In a related vein, Python class members are always public. In Ruby, fields are always private; more than that, they are accessible only to methods of the individual object to which they belong.
与public、private或protected所暗示的可见性正交,大多数面向对象语言允许将各个字段和方法声明为static。静态类成员被认为“属于”整个类,而不是任何单个对象。因此,它们有时被称为类字段和方法,而不是实例字段和方法。(此术语在创建特殊元对象来表示每个类的语言中最常见 - 参见示例 10.26。类字段和方法被认为属于元对象。)每个静态字段的单一副本由其类的所有实例共享:一个对象的方法中对该字段所做的更改将对该类的所有其他对象的方法可见。静态方法没有 this 参数(显式或隐式);它不能访问非静态(实例)字段。另一方面,非静态(实例)方法可以访问静态和非静态字段。
Orthogonal to the visibility implied by public, private, or protected, most object-oriented languages allow individual fields and methods to be declared static. Static class members are thought of as “belonging” to the class as a whole, not to any individual object. They are therefore sometimes referred to as class fields and methods, as opposed to instance fields and methods. (This terminology is most common in languages that create a special metaobject to represent each class—see Example 10.26. The class fields and methods are thought of as belonging to the metaobject.) A single copy of each static field is shared by all instances of its class: changes made to that field in methods of one object will be visible to methods of all other objects of the class. A static method, for its part, has no this parameter (explicit or implicit); it cannot access nonstatic (instance) fields. A nonstatic (instance) method, on the other hand, can access both static and nonstatic fields.
Java 中的内部类和本地类被广泛用于创建对象闭包,如第 3.6.3 节所述。在第 9.6.2 节中,我们将它们用作事件处理程序。我们还注意到,Java 中的本地类可以是匿名的:它可以以内联方式出现在对new 的调用中(示例 9.54)。
Inner and local classes in Java are widely used to create object closures, as described in Section 3.6.3. In Section 9.6.2 we used them as handlers for events. We also noted that a local class in Java can be anonymous: it can appear, in-line, inside a call to new (Example 9.54).
Smalltalk、Objective-C、Eiffel、C++、Java 和 C# 都是从一开始就设计为面向对象的语言,要么从头开始,要么从没有强大封装机制的现有语言开始。它们都支持模块作为类型的抽象方法,其中单一机制(类)同时提供封装和继承。其他几种语言,包括 Modula-3 和 Oberon(均为 Modula-2 的后继者)、CLOS、Ada 95/2005 和 Fortran 2003,可以被描述为对模块已经提供封装的语言的面向对象扩展。这些语言不会改变现有的模块机制,而是通过扩展记录的机制提供继承和动态方法绑定。
Smalltalk, Objective-C, Eiffel, C++, Java, and C# were all designed from the outset as object-oriented languages, either starting from scratch or from an existing language without a strong encapsulation mechanism. They all support a module-as-type approach to abstraction, in which a single mechanism (the class) provides both encapsulation and inheritance. Several other languages, including Modula-3 and Oberon (both successors to Modula-2), CLOS, Ada 95/2005, and Fortran 2003, can be characterized as object-oriented extensions to languages in which modules already provide encapsulation. Rather than alter the existing module mechanism, these languages provide inheritance and dynamic method binding through a mechanism for extending records.
扩展现有抽象的功能是面向对象编程的主要动机之一。继承是使这种扩展成为可能的标准机制。但是,有时继承不是一种选择,特别是在处理现有代码时。想要扩展的类可能不允许继承,例如:在 Java 中,它可能被标记为final;在 C# 中,它可能被标记为sealed。即使原则上可以继承,也可能有大量现有代码使用原始类名,并且返回并更改所有变量和参数声明以使用新的派生类型可能不可行。
The desire to extend the functionality ofan existing abstraction is one ofthe principal motivations for object-oriented programming. Inheritance is the standard mechanism that makes such extension possible. There are times, however, when inheritance is not an option, particularly when dealing with preexisting code. The class one wants to extend may not permit inheritance, for instance: in Java, it may be labeled final; in C#, it may be sealed. Even if inheritance is possible in principle, there may be a large body of existing code that uses the original class name, and it may not be feasible to go back and change all the variable and parameter declarations to use a new derived type.
扩展方法没有特殊功能。具体来说,它们无法访问其扩展类的私有成员,也不支持动态方法绑定(第 10.4 节)。相比之下,包括 JavaScript 和 Ruby 在内的几种脚本语言确实允许程序员向现有类甚至单个对象添加新方法。我们将在第 14.4.4 节中进一步探讨这些选项。
No special functionality is available to extension methods. In particular, they cannot access private members of the class that they extend, nor do they support dynamic method binding (Section 10.4). By contrast, several scripting languages, including JavaScript and Ruby, really do allow the programmer to add new methods to existing classes—or even to individual objects. We will explore these options further in Section 14.4.4.
在第 3.2 节中,我们将对象的生命周期定义为对象占用空间并因此可以保存数据的时间间隔。大多数面向对象语言都提供了某种特殊机制,可以在对象生命周期开始时自动初始化对象。当以子程序形式编写时,此机制称为构造函数。虽然名称可能被认为暗示了其他含义,但构造函数不会分配空间;它会初始化已经分配的空间。一些语言提供了类似的析构函数机制,可以在对象生命周期结束时自动终止对象。出现了几个重要问题:
In Section 3.2 we defined the lifetime of an object to be the interval during which it occupies space and can thus hold data. Most object-oriented languages provide some sort of special mechanism to initialize an object automatically at the beginning of its lifetime. When written in the form of a subroutine, this mechanism is known as a constructor. Though the name might be thought to imply otherwise, a constructor does not allocate space; it initializes space that has already been allocated. A few languages provide a similar destructor mechanism to finalize an object automatically at the end of its lifetime. Several important issues arise:
选择构造函数:面向对象语言可能允许一个类具有零个、一个或多个不同的构造函数。在后一种情况下,不同的构造函数可能具有不同的名称,或者可能需要通过参数的数量和类型来区分它们。
Choosing a constructor: An object-oriented language may permit a class to have zero, one, or many distinct constructors. In the latter case, different constructors may have different names, or it may be necessary to distinguish among them by number and types of arguments.
引用和值:如果变量是引用,那么每个对象都必须显式创建,并且很容易确保调用适当的构造函数。如果变量是值,那么对象的创建可以作为细化的结果而隐式发生。在后一种情况下,语言必须允许对象在未初始化的情况下开始其生命周期,或者必须为每个精心设计的对象提供一种选择合适构造函数的方法。
References and values: If variables are references, then every object must be created explicitly, and it is easy to ensure that an appropriate constructor is called. If variables are values, then object creation can happen implicitly as a result of elaboration. In this latter case, the language must either permit objects to begin their lifetime uninitialized, or it must provide a way to choose an appropriate constructor for every elaborated object.
执行顺序:在 C++ 中创建派生类的对象时,编译器保证所有基类的构造函数都将先执行最外层的,然后再执行派生类的构造函数。此外,如果某个类的成员本身是某个类的对象,那么这些成员的构造函数将在包含它们的对象的构造函数之前被调用。这些规则是语法和语义复杂性的来源:当与多个构造函数、详尽的对象和多重继承相结合时,它们有时会在控制进入给定范围之前引发复杂的嵌套构造函数调用序列,并进行过载解析。其他语言的规则更简单。
Execution order: When an object of a derived class is created in C++, the compiler guarantees that the constructors for any base classes will be executed, outermost first, before the constructor for the derived class. Moreover, if a class has members that are themselves objects of some class, then the constructors for the members will be called before the constructor for the object in which they are contained. These rules are a source of considerable syntactic and semantic complexity: when combined with multiple constructors, elaborated objects, and multiple inheritance, they can sometimes induce a complicated sequence of nested constructor invocations, with overload resolution, before control even enters a given scope. Other languages have simpler rules.
垃圾收集:大多数面向对象语言都提供某种构造函数机制。析构函数相对较少见。它们的主要目的是方便 C++ 等语言中的手动存储回收。如果语言实现自动收集垃圾,那么对析构函数的需求就会大大减少。
Garbage collection: Most object-oriented languages provide some sort of constructor mechanism. Destructors are comparatively rare. Their principal purpose is to facilitate manual storage reclamation in languages like C++. If the language implementation collects garbage automatically, then the need for destructors is greatly reduced.
在本节的其余部分,我们将更详细地讨论这些问题。
In the remainder of this section we consider these issues in more detail.
Smalltalk 在使用多个命名构造函数方面与 Eiffel 相似,但它更明确地区分属于单个对象的操作和属于一类对象的操作。Smalltalk 还采用了一种拟人化编程模型,其中每个操作都看作是由某个特定对象响应来自其他对象的请求(“消息”)而执行的。由于对象 O创建自身没有多大意义,因此O必须由代表O所属类的其他对象(称之为C)创建。当然,因为C是一个对象,所以它本身必须属于某个类。这种推理的结果是一个系统,其中每个类定义实际上都会引入一对类和一对对象来表示它们。Objective-C 和 CLOS 具有类似的双重层次结构,Python 和 Ruby 也是如此。
Smalltalk resembles Eiffel in the use of multiple named constructors, but it distinguishes more sharply between operations that pertain to an individual object and operations that pertain to a class of objects. Smalltalk also adopts an anthropomorphic programming model in which every operation is seen as being executed by some specific object in response to a request (a “message”) from some other object. Since it makes little sense for an object O to create itself, O must be created by some other object (call it C) that represents O's class. Of course, because C is an object, it must itself belong to some class. The result of this reasoning is a system in which each class definition really introduces a pair of classes and a pair of objects to represent them. Objective-C and CLOS have similar dual hierarchies, as do Python and Ruby.
一些历史悠久的语言(尤其是 Modula-3 和 Oberon)根本没有提供构造函数:程序员必须明确地初始化所有内容。Ada 95仅支持对从标准库类型Controlled派生的类型的对象自动调用构造函数和析构函数(Initialize和Finalize例程) 。
A few historic languages—notably Modula-3 and Oberon—provided no constructors at all: the programmer had to initialize everything explicitly. Ada 95 supports automatic calls to constructors and destructors (Initialize and Finalize routines) only for objects of types derived from the standard library type Controlled.
许多面向对象语言(包括 Simula、Smalltalk、Python、Ruby 和 Java)都使用一种编程模型,其中变量引用对象。少数语言(包括 C++ 和 Ada)允许变量具有对象值。Eiffel默认使用引用模型,但允许程序员指定某些类应扩展,在这种情况下,这些类的变量将使用值模型。类似地,C# 和 Swift 使用struct来定义变量为值的类型,使用class来定义变量为引用的类型。
Many object-oriented languages, including Simula, Smalltalk, Python, Ruby, and Java, use a programming model in which variables refer to objects. A few languages, including C++ and Ada, allow a variable to have a value that is an object. Eiffel uses a reference model by default, but allows the programmer to specify that certain classes should be expanded, in which case variables of those classes will use a value model. In a similar vein, C# and Swift use struct to define types whose variables are values, and class to define types whose variables are references.
使用变量的引用模型,每个对象都是显式创建的,并且很容易确保调用适当的构造函数。使用变量的值模型,对象创建可以作为细化的结果而隐式发生。在默认情况下不提供对构造函数的自动调用的 Ada 中,细化对象从一开始就未初始化,并且可能会意外尝试在变量具有值之前使用它。在 C++ 中,编译器确保为每个细化对象调用适当的构造函数,但它用于识别构造函数及其参数的规则有时会令人困惑。
With a reference model for variables, every object is created explicitly, and it is easy to ensure that an appropriate constructor is called. With a value model for variables, object creation can happen implicitly as a result of elaboration. In Ada, which doesn't provide automatic calls to constructors by default, elaborated objects begin life uninitialized, and it is possible to accidentally attempt to use a variable before it has a value. In C++, the compiler ensures that an appropriate constructor is called for every elaborated object, but the rules it uses to identify constructors and their arguments can sometimes be confusing.
因为 Java 对所有对象使用统一的引用模型,所以任何本身是对象的类成员实际上都是引用,而不是“扩展”对象(使用 Eiffel 术语)。Java 只是将这些成员初始化为null。如果程序员想要不同的东西,他或她必须在周围类的构造函数中显式调用new。Smalltalk和(在常见情况下)C# 和 Eiffel 采用类似的方法。在 C# 中,类型为结构体的成员通过将其所有字段设置为零或 null 来初始化。在 Eiffel 中,如果类包含扩展类类型的成员,则该类型需要有一个没有参数的构造函数;Eiffel 编译器会在创建周围对象时安排调用此构造函数。
Because Java uses a reference model uniformly for all objects, any class members that are themselves objects will actually be references, rather than “expanded” objects (to use the Eiffel term). Java simply initializes such members to null. If the programmer wants something different, he or she must call new explicitly within the constructor of the surrounding class. Smalltalk and (in the common case) C# and Eiffel adopt a similar approach. In C#, members whose types are structs are initialized by setting all of their fields to zero or null. In Eiffel, if a class contains members of an expanded class type, that type is required to have a single constructor, with no arguments; the Eiffel compiler arranges to call this constructor when the surrounding object is created.
Smalltalk、Eiffel、CLOS 和 Objective-C 在基类初始化方面都比 C++ 宽松。编译器或解释器会自动调用每个新创建对象的构造函数(创建器、初始化器),但不会自动调用基类的构造函数;它所做的只是将基类数据成员初始化为默认值(零或null)。如果派生类想要不同的行为,其构造函数必须明确调用基类的构造函数。
Smalltalk, Eiffel, CLOS, and Objective-C are all more lax than C++ regarding the initialization of base classes. The compiler or interpreter arranges to call the constructor (creator, initializer) for each newly created object automatically, but it does not arrange to call constructors for base classes automatically; all it does is initialize base class data members to default (zero or null) values. If the derived class wants different behavior, its constructor(s) must call a constructor for the base class explicitly.
在具有自动垃圾收集功能的语言中,对析构函数的需求要小得多。事实上,在具有垃圾收集功能的语言中,整个析构概念都是值得怀疑的,因为程序员几乎无法控制对象何时被销毁。Java 和 C# 允许程序员声明一个finalize方法,该方法将在垃圾收集器回收对象空间之前立即调用,但该功能并未得到广泛使用。
In languages with automatic garbage collection, there is much less need for destructors. In fact, the entire idea of destruction is suspect in a garbage-collected language, because the programmer has little or no control over when an object is going to be destroyed. Java and C# allow the programmer to declare a finalize method that will be called immediately before the garbage collector reclaims the space for an object, but the feature is not widely used.
继承/类型扩展的主要后果之一是派生类D拥有其基类C的所有成员(数据和子程序)。只要D不隐藏C的任何公开可见成员(参见练习 10.15 ),允许在任何需要 C 类对象的上下文中使用类D的对象都是有意义的:我们对C类对象所做的任何操作,我们都可以对类D的对象执行。换句话说,不隐藏其基类的任何公开可见成员的派生类是该基类的子类型。
One of the principal consequences of inheritance/type extension is that a derived class D has all the members—data and subroutines—of its base class C. As long as D does not hide any of the publicly visible members of C (see Exercise 10.15), it makes sense to allow an object of class D to be used in any context that expects an object of class C: anything we might want to do to an object of class C we can also do to an object of class D. In other words, a derived class that does not hide any publicly visible members of its base class is a subtype of that base class.
第一个选项(使用引用的类型)称为静态方法绑定。第二个选项(使用对象的类)称为动态方法绑定。动态方法绑定是面向对象编程的核心。例如,想象一下我们的管理计算程序创建了一个图书馆图书逾期未还的人员名单。该名单可能包含学生和教授。如果我们遍历该列表并为每个人打印邮寄标签,动态方法绑定将确保为每个人调用正确的打印例程。在这种情况下,派生类中的定义被称为覆盖基类中的定义。
The first option (use the type of the reference) is known as static method binding. The second option (use the class of the object) is known as dynamic method binding. Dynamic method binding is central to object-oriented programming. Imagine, for example, that our administrative computing program has created a list of persons who have overdue library books. The list may contain both students and professors. If we traverse the list and print a mailing label for each person, dynamic method binding will ensure that the correct printing routine is called for each individual. In this situation the definitions in the derived classes are said to override the definition in the base class.
不幸的是,正如我们将在第 10.4.3 节中看到的,动态方法绑定会产生运行时开销。虽然这种开销通常不大,但对于性能至关重要的应用程序中的小子程序来说,这仍然是一个问题。Smalltalk、Objective-C、Python 和 Ruby 对所有方法都使用动态方法绑定。Java 和 Eiffel 默认使用动态方法绑定,但允许将各个方法和(在 Java 中)类标记为final(Java)或freeze(Eiffel),在这种情况下,它们不能被派生类覆盖,因此可以采用优化的实现。Simula、C++、C# 和 Ada 95 默认使用静态方法绑定,但允许程序员在需要时指定动态绑定。在后一种语言中,区分覆盖使用动态绑定的方法和(仅仅)重新定义使用静态绑定的方法是常用的术语。为了清楚起见,每当派生类中的方法覆盖或重新定义基类中同名的方法时, C# 都要求明确使用关键字override和new。Java和 C++11 具有类似的注释,鼓励但不要求使用它们。
Unfortunately, as we shall see in Section 10.4.3, dynamic method binding imposes run-time overhead. While this overhead is generally modest, it is nonetheless a concern for small subroutines in performance-critical applications. Smalltalk, Objective-C, Python, and Ruby use dynamic method binding for all methods. Java and Eiffel use dynamic method binding by default, but allow individual methods and (in Java) classes to be labeled final (Java) or frozen (Eiffel), in which case they cannot be overridden by derived classes, and can therefore employ an optimized implementation. Simula, C++, C#, and Ada 95 use static method binding by default, but allow the programmer to specify dynamic binding when desired. In these latter languages it is common terminology to distinguish between overriding a method that uses dynamic binding and (merely) redefining a method that uses static binding. For the sake of clarity, C# requires explicit use of the keywords override and new whenever a method in a derived class overrides or redefines (respectively) a method of the same name in a base class. Java and C++11 have similar annotations whose use is encouraged but not required.
无论声明语法如何,如果一个类至少有一个抽象方法,则该类被称为抽象类。无法声明抽象类的对象,因为它至少缺少一个成员。抽象类的唯一目的是作为其他具体类的基类。具体类(或其中间祖先之一)必须为其继承的每个抽象方法提供实际定义。基类中抽象方法的存在为动态方法绑定提供了“钩子”;它允许程序员编写调用基类对象的方法(引用)的代码,前提是适当的具体方法将在运行时被调用。除了抽象方法之外没有其他成员的类(没有字段或方法体)在 Java、C# 和 Ada 2005 中称为接口。它们支持受限制的“混合”形式的多重继承,我们将在第10.5 节中讨论。6
Regardless of declaration syntax, a class is said to be abstract if it has at least one abstract method. It is not possible to declare an object of an abstract class, because it would be missing at least one member. The only purpose of an abstract class is to serve as a base for other, concrete classes. A concrete class (or one of its intermediate ancestors) must provide a real definition for every abstract method it inherits. The existence of an abstract method in a base class provides a “hook” for dynamic method binding; it allows the programmer to write code that calls methods of (references to) objects of the base class, under the assumption that appropriate concrete methods will be invoked at run time. Classes that have no members other than abstract methods—no fields or method bodies—are called interfaces in Java, C#, and Ada 2005. They support a restricted, “mix-in” form of multiple inheritance, which we will consider in Section 10.5.6
如第 7.3 节所述,Smalltalk 采用“鸭子类型”:变量是无类型的引用,对任何对象的引用都可以赋值给任何变量。只有当代码在运行时实际尝试调用操作(发送“消息”)时,语言实现才会检查对象是否支持该操作;如果支持,则认为对象的类型是可以接受的。实现很简单:对象的字段永远不会公共;方法提供了对象交互的唯一方式。对象的表示以类型描述符的地址开始。类型描述符包含一个将方法名称映射到代码片段的字典。在运行时,Smalltalk 解释器在字典中执行查找操作以查看是否支持该方法。如果不支持,它会生成“消息无法理解”错误 - 相当于 Lisp 中的类型冲突错误。CLOS、Objective-C、Swift 和面向对象的脚本语言提供类似的语义,并邀请类似的实现。动态方法可以说比静态方法更灵活,但当方法较小时,它会带来显着的成本,并延迟错误报告。
As noted in Section 7.3, Smalltalk employs “duck typing”: variables are untyped references, and a reference to any object may be assigned into any variable. Only when code actually attempts to invoke an operation (send a “message”) at run time does the language implementation check to see whether the operation is supported by the object; if so, the object's type is assumed to be acceptable. The implementation is straightforward: fields of an object are never public; methods provide the only means of object interaction. The representation of an object begins with the address of a type descriptor. The type descriptor contains a dictionary that maps method names to code fragments. At run time, the Smalltalk interpreter performs a lookup operation in the dictionary to see if the method is supported. If not, it generates a “message not understood” error—the equivalent of a type-clash error in Lisp. CLOS, Objective-C, Swift, and the object-oriented scripting languages provide similar semantics, and invite similar implementations. The dynamic approach is arguably more flexible than the static, but it imposes significant cost when methods are small, and delays the reporting of errors.
除了增加间接开销之外,虚拟方法通常还会妨碍编译时内联扩展子例程。当子例程较小且调用频繁时,缺少内联子例程可能会带来严重的性能问题。与 C 一样,C++ 尽可能避免运行时开销:因此它默认使用静态方法绑定,并且严重依赖对象值变量,因此甚至可以在编译时分派虚拟方法。
In addition to imposing the overhead of indirection, virtual methods often preclude the in-line expansion of subroutines at compile time. The lack of in-line subroutines can be a serious performance problem when subroutines are small and frequently called. Like C, C++ attempts to avoid run-time overhead whenever possible: hence its use of static method binding as the default, and its heavy reliance on object-valued variables, for which even virtual methods can be dispatched at compile time.
对象闭包在 Java(以及其他几种语言)中被广泛用于封装新创建的控制线程的启动参数(有关详细信息,请参阅第 13.2.3 节)。它们还可用于(如探索 6.46中所述)通过访问者模式实现迭代器。
Object closures are commonly used in Java (and several other languages) to encapsulate start-up arguments for newly created threads of control (more on this in Section 13.2.3). They can also be used (as noted in Exploration 6.46) to implement iterators via the visitor pattern.
在构建面向对象系统时,设计一个完美的继承树通常很困难,其中每个类都只有一个父类。猫可能是动物、宠物、家庭成员或情感对象。公司数据库中的小部件可能是可排序对象(从报告系统的角度来看)、可图形对象(从窗口系统的角度来看)或可存储对象(从文件系统的角度来看);我们如何选择一个?
When building an object-oriented system, it is often difficult to design a perfect inheritance tree, in which every class has exactly one parent. A cat may be an animal, a pet, a family_member, or an object_of_affection. A widget in the company database maybe a sortable_object (from the reporting system's perspective), a graphable_object (from the window system's perspective), or a storable_object (from the file system's perspective); how do we choose just one?
在一般情况下,我们可以想象一个类有任意数量的父类,每个父类都可以为其提供字段和方法(抽象和具体)。这种“真正的”多重继承由多种语言提供,包括 C++、Eiffel、CLOS、OCaml 和 Python;我们将在第10.6 节中讨论它。不幸的是,它在语言语义和运行时实现方面都引入了相当大的复杂性。在实践中,一种更有限的机制,称为混合继承,往往是我们真正需要的。
In the general case, we could imagine allowing a class to have an arbitrary number of parents, each of which could provide it with both fields and methods (both abstract and concrete). This sort of “true” multiple inheritance is provided by several languages, including C++, Eiffel, CLOS, OCaml, and Python; we will consider it in Section 10.6. Unfortunately, it introduces considerable complexity in both language semantics and run-time implementation. In practice, a more limited mechanism, known as mix-in inheritance, is often all we really need.
实际上,正如我们在10.4.2 节中提到的那样,接口是只包含抽象方法的类,没有字段或方法体。只要它只从一个“真实”父类继承,类就可以“混入”任意数量的接口。如果子例程的形式参数被声明为具有接口类型,那么实现(继承自)该接口的任何类都可以作为相应的实际参数传递。可以合法传递的对象类不需要具有共同的类祖先。
In effect—as we noted in Section 10.4.2—an interface is a class containing only abstract methods—no fields or method bodies. So long as it inherits from only one “real” parent, a class can “mix in” an arbitrary number of interfaces. If a formal parameter of a subroutine is declared to have an interface type, then any class that implements (inherits from) that interface can be passed as the corresponding actual parameter. The classes of objects that can legitimately be passed need not have a common class ancestor.
近年来,混合式已成为实现多重继承的常用方法(可以说是主流方法)。尽管不同语言之间的细节各不相同,但 Java、C#、Scala、Objective-C、Swift、Go、Ada 2005 和 Ruby 等语言中都出现了接口。
In recent years, mix-ins have become a common approach—arguably the dominant approach—to multiple inheritance. Though details vary from one language to another, interfaces appear in Java, C#, Scala, Objective-C, Swift, Go, Ada 2005, and Ruby, among others.
在 Ruby、Objective-C 或 Swift 等使用动态方法查找的语言中,接口的方法可以简单地添加到实现该接口的任何类的方法字典中。在任何需要接口类型的上下文中,通常的查找机制都会找到合适的方法。在具有完全静态类型的语言中,方法指针应该位于已知的 vtable 偏移量处,因此需要新的机制。挑战归结为需要对对象进行多种查看。
In a language like Ruby, Objective-C, or Swift, which uses dynamic method lookup, the methods of an interface can simply be added to the method dictionary of any class that implements the interface. In any context that requires the interface type, the usual lookup mechanism will find the proper methods. In a language with fully static typing, in which pointers to methods are expected to lie at known vtable offsets, new machinery is required. The challenge boils down to a need for multiple views of an object.
上述接口描述反映了 Java 的历史版本,但有一点被忽略:除了抽象方法之外,接口还可以定义静态 final(常量)字段。由于此类字段永远不会改变,因此不会引入运行时复杂性或开销 — 编译器可以在使用它们的任何地方有效地就地扩展它们。
The description of interfaces above reflects historical versions of Java, with one omission: in addition to abstract methods, an interface can define static final (constant) fields. Because such fields can never change, they introduce no runtime complexity or overhead—the compiler can, effectively, expand them in place wherever they are used.
从 Java 8 开始,接口也得到了扩展,允许使用静态方法和默认方法,这两种方法在接口声明中都有主体(代码)。静态方法(如静态 final字段)不会引入实现复杂性:它不需要访问对象字段,因此不会产生将哪个视图作为this传递的歧义—没有this参数。默认方法有点棘手。它们的代码旨在供任何未覆盖它的类使用。这种约定对于库来说尤其有价值维护者:它允许将新方法添加到现有的库接口中,而不会破坏现有的用户代码,否则必须更新现有用户代码才能在从该接口继承的任何类中实现新方法。
Beginning with Java 8, interfaces have also been extended to allow static and default methods, both of which are given bodies—code—in the declaration of the interface. A static method, like a static final field, introduces no implementation complexity: it requires no access to object fields, so there is no ambiguity about what view to pass as this—there is no this parameter. Default methods are a bit more tricky. Their code is intended to be used by any class that does not override it. This convention is particularly valuable for library maintainers: it allows new methods to be added to an existing library interface without breaking existing user code, which would otherwise have to be updated to implement the new methods in any class that inherits from the interface.
事实证明,Scala 编程语言早已提供了与默认方法等效的功能,其混合函数称为特征。实际上,特征不仅支持默认方法,还支持可变字段。Scala 编译器不会尝试创建一个视图来使这些字段可直接访问,而是为每个继承自特征的具体类生成一对隐藏的访问器方法,类似于C# 的属性(示例 10.7)。然后,对访问器方法的引用将包含在接口特定的 vtable 中,在那里它们可以通过默认方法调用。在任何未提供自己的特征字段定义的类中,编译器都会创建一个新的私有字段供访问器方法使用。
As it turns out, the equivalent of default methods has long been provided by the Scala programming language, whose mix-ins are known as traits. In fact, traits support not only default methods but also mutable fields. Rather than try to create a view that would make these fields directly accessible, the Scala compiler generates, for each concrete class that inherits from the trait, a pair of hidden accessor methods analogous to the properties of C# (Example 10.7). References to the accessor methods are then included in the interface-specific vtable, where they can be called by default methods. In any class that does not provide its own definition of a trait field, the compiler creates a new private field to be used by the accessor methods.
如第 10.5 节所述,混合继承允许接口指定继承类必须提供的功能,以便该类的对象可以在给定上下文中使用。至关重要的是,接口在大多数情况下并不提供该功能本身。即使是默认方法也主要用于协调对继承类提供的功能的访问。
As described in Section 10.5, mix-in inheritance allows an interface to specify functionality that must be provided by an inheriting class in order for objects of that class to be used in a given context. Crucially, an interface does not, for the most part, provide that functionality itself. Even default methods serve mainly to orchestrate access to functionality provided by the inheriting class.
真正的多重继承也出现在其他几种语言中,包括 CLOS、OCaml 和 Python。许多较老的语言,包括 Simula、Smalltalk、Modula-3 和 Oberon,只提供单一继承。混合继承是一种常见的折衷方案。
True multiple inheritance appears in several other languages as well, including CLOS, OCaml, and Python. Many older languages, including Simula, Smalltalk, Modula-3, and Oberon, provided only single inheritance. Mix-in inheritance is a common compromise.
更深入地
IN MORE DEPTH
多重继承引入了大量的语义和实际问题,我们在配套站点上对此进行了考虑:
Multiple inheritance introduces a wealth of semantic and pragmatic issues, which we consider on the companion site:
■ 假设两个父类提供了同名的方法。我们在子类中使用哪一个?我们可以同时访问这两个方法吗?
■ Suppose two parent classes provide a method with the same name. Which one do we use in the child? Can we access both?
■ 假设两个父类都派生自某个共同的“祖父”类。“孙子”类是否有祖父类字段的一个副本或两个副本?
■ Suppose two parent classes are both derived from some common “grandparent” class. Does the “grandchild” have one copy or two of the grandparent's fields?
■ 我们实现单继承依赖于这样一个事实:父类对象的表示是派生类对象的表示的前缀。在多重继承中,每个父类如何成为子类的前缀?
■ Our implementation of single inheritance relies on the fact that the representation of an object of the parent class is a prefix of the representation of an object of a derived class. With multiple inheritance, how can each parent be a prefix of the child?
具有共同“祖父母”的多重继承称为重复继承。具有祖父母单独副本的重复继承称为复制继承;具有祖父母单个副本的重复继承称为共享继承。共享继承是 Eiffel 中的默认设置。复制继承是 C++ 中的默认设置。这两种语言都允许程序员在需要时获得另一个选项。
Multiple inheritance with a common “grandparent” is known as repeated inheritance. Repeated inheritance with separate copies of the grandparent is known as replicated inheritance; repeated inheritance with a single copy of the grandparent is known as shared inheritance. Shared inheritance is the default in Eiffel. Replicated inheritance is the default in C++. Both languages allow the programmer to obtain the other option when desired.
在本章开头,我们用三个基本概念来描述面向对象编程:封装、继承和动态方法绑定。封装允许将抽象的实现细节隐藏在简单的接口后面。继承允许将新抽象定义为某些现有抽象的扩展或细化,从而自动获得其部分或全部特性。动态方法绑定允许新抽象即使在需要旧抽象的上下文中使用时也能显示其新行为。
At the beginning of this chapter, we characterized object-oriented programming in terms of three fundamental concepts: encapsulation, inheritance, and dynamic method binding. Encapsulation allows the implementation details of an abstraction to be hidden behind a simple interface. Inheritance allows a new abstraction to be defined as an extension or refinement of some existing abstraction, obtaining some or all of its characteristics automatically. Dynamic method binding allows the new abstraction to display its new behavior even when used in a context that expects the old abstraction.
不同的编程语言对这些基本概念的支持程度不同。具体来说,语言在要求程序员以面向对象风格编写的程度上有所不同。一些作者认为,真正的面向对象语言应该使编写非面向对象的程序变得困难或不可能。从这个纯粹主义的观点来看,面向对象语言应该呈现统一的计算对象模型,其中每种数据类型都是一个类,每个变量都是对对象的引用,每个子例程都是一个对象方法。此外,应该以拟人化的方式考虑对象:作为负责所有计算的活动实体。
Different programming languages support these fundamental concepts to different degrees. In particular, languages differ in the extent to which they require the programmer to write in an object-oriented style. Some authors argue that a truly object-oriented language should make it difficult or impossible to write programs that are not object-oriented. From this purist point of view, an object-oriented language should present a uniform object model of computing, in which every data type is a class, every variable is a reference to an object, and every subroutine is an object method. Moreover, objects should be thought of in anthropomorphic terms: as active entities responsible for all computation.
Smalltalk 和 Ruby 接近这一理想。事实上,正如下面小节(主要在配套网站上)所述,在 Smalltalk 中,甚至诸如选择和迭代之类的控制流机制也被建模为方法调用。另一方面,Ada 95 和 Fortran 2003 可能最好被描述为冯·诺依曼语言,允许程序员在需要时以面向对象风格编写代码。
Smalltalk and Ruby come close to this ideal. In fact, as described in the subsection below (mostly on the companion site), even such control-flow mechanisms as selection and iteration are modeled as method invocations in Smalltalk. On the other hand, Ada 95 and Fortran 2003 are probably best characterized as von Neumann languages that permit the programmer to write in an object-oriented style if desired.
那么 C++ 又如何呢?它确实具有丰富的特性,包括面向对象程序中很有用的、Smalltalk 所没有的几个特性(多重继承、复杂的访问控制、严格的初始化顺序、析构函数、泛型)。与此同时,它还存在许多问题。它的简单类型不是类。它在类之外有子例程。它默认使用静态方法绑定和复制多重继承,而不是成本更高的虚拟替代方案。它的未经检查的 C 风格类型转换为类型检查和访问控制提供了重大漏洞。它缺乏垃圾收集,这是创建正确、自足的抽象的主要障碍。可能最严重的是,C++ 保留了 C 的所有低级机制,允许程序员逃避或颠覆面向对象的编程模型完全不同。有人认为,最好的 C++ 程序员是那些没有先学习 C 语言的人:他们不太想用新语言编写“C 风格”的程序。总的来说,可以说 C++ 是一种面向对象的语言,就像 Common Lisp 是一种函数式语言一样。除了垃圾收集之外,C++ 提供了所有必要的工具,但程序员需要大量的纪律才能“正确”使用这些工具。
So what about C++? It certainly has a wealth of features, including several (multiple inheritance, elaborate access control, strict initialization order, destructors, generics) that are useful in object-oriented programs and that are not found in Smalltalk. At the same time, it has a wealth of problematic wrinkles. Its simple types are not classes. It has subroutines outside of classes. It uses static method binding and replicated multiple inheritance by default, rather than the more costly virtual alternatives. Its unchecked C-style type casts provide a major loophole for type checking and access control. Its lack of garbage collection is a major obstacle to the creation of correct, self-contained abstractions. Probably most serious of all, C++ retains all of the low-level mechanisms of C, allowing the programmer to escape or subvert the object-oriented model of programming entirely. It has been suggested that the best C++ programmers are those who did not learn C first: they are not as tempted to write “C-style” programs in the newer language. On balance, it is probably safe to say that C++ is an object-oriented language in the same sense that Common Lisp is a functional language. With the possible exception of garbage collection, C++ provides all of the necessary tools, but it requires substantial discipline on the part of the programmer to use those tools “correctly.”
从历史上看,Smalltalk 被认为是面向对象语言的典范。该语言的原始版本由 Alan Kay 在 20 世纪 60 年代末作为犹他大学博士工作的一部分设计。它随后被施乐帕洛阿尔托研究中心 (PARC) 的软件概念小组采用,并在 20 世纪 70 年代经历了五次重大修订,最终形成了 Smalltalk-80 语言。7
Historically, Smalltalk was considered the canonical object-oriented language. The original version of the language was designed by Alan Kay as part of his doctoral work at the University of Utah in the late 1960s. It was then adopted by the Software Concepts Group at the Xerox Palo Alto Research Center (PARC), and went through five major revisions in the 1970s, culminating in the Smalltalk-80 language.7
更深入地
IN MORE DEPTH
我们在前面的章节中提到了 Smalltalk 的几个特性。在配套网站上可以找到更长的介绍,我们特别关注 Smalltalk 的拟人化编程模型。对该语言的完整介绍超出了本书的范围。
We have mentioned several features of Smalltalk in previous sections. A somewhat longer treatment can be found on the companion site, where we focus in particular on Smalltalk's anthropomorphic programming model. A full introduction to the language is beyond the scope of this book.
这是我们关于语言设计的六个核心章节中的最后一章:名称(第 3 章)、控制流(第 6 章)、类型系统(第 7 章)、复合类型(第 8 章)、子程序(第 9 章)和对象(第 10 章)。
This has been the last of our six core chapters on language design: names (Chapter 3), control flow (Chapter 6), type systems (Chapter 7), composite types (Chapter 8), subroutines (Chapter 9), and objects (Chapter 10).
我们从10.1 节开始,介绍了面向对象编程的三个基本概念:封装、继承和动态方法绑定。我们还介绍了类、对象和方法的术语。我们已经在第 3 章的模块中看到了封装。封装允许将复杂数据抽象的细节隐藏在相对简单的接口后面。继承扩展了封装的实用性,使程序员可以轻松地将新的抽象定义为现有抽象的改进或扩展。继承为多态子程序提供了自然的基础:如果子程序期望给定类的实例作为参数,那么可以使用从预期类派生的任何类的对象(假设它保留了整个现有接口)。动态方法绑定通过安排对参数方法之一的调用在运行时使用与实际对象的类相关联的实现,而不是与参数的声明类相关联的实现,扩展了这种形式的多态性。我们注意到,一些语言,包括 Modula-3、Oberon、Ada 95 和 Fortran 2003,通过类型扩展机制支持面向对象,其中封装与模块相关,但继承和动态方法绑定与特殊形式的记录相关。
We began in Section 10.1 by identifying three fundamental concepts of object-oriented programming: encapsulation, inheritance, and dynamic method binding. We also introduced the terminology of classes, objects, and methods. We had already seen encapsulation in the modules of Chapter 3. Encapsulation allows the details of a complicated data abstraction to be hidden behind a comparatively simple interface. Inheritance extends the utility of encapsulation by making it easy for programmers to define new abstractions as refinements or extensions of existing abstractions. Inheritance provides a natural basis for polymorphic subroutines: if a subroutine expects an instance of a given class as argument, then an object of any class derived from the expected one can be used instead (assuming that it retains the entire existing interface). Dynamic method binding extends this form of polymorphism by arranging for a call to one of the parameter's methods to use the implementation associated with the class of the actual object at run time, rather than the implementation associated with the declared class of the parameter. We noted that some languages, including Modula-3, Oberon, Ada 95, and Fortran 2003, support object orientation through a type extension mechanism, in which encapsulation is associated with modules, but inheritance and dynamic method binding are associated with a special form of record.
在后面的章节中,我们详细介绍了对象初始化和终止、动态方法绑定和(在配套站点上)多重继承。在许多情况下,我们发现功能性和简单性与执行速度之间存在权衡。将变量视为引用而不是值通常会产生更简单的语义,但需要额外的间接性。如前文第8.5.3 节所述,垃圾收集大大简化了软件的创建和维护,但会产生运行时成本。动态方法绑定要求(在一般情况下)使用 vtable 或其他查找机制来调度方法。多重继承的完全通用实现往往会在未使用时产生开销。
In later sections we covered object initialization and finalization, dynamic method binding, and (on the companion site) multiple inheritance in some detail. In many cases we discovered tradeoffs between functionality on the one hand and simplicity and execution speed on the other. Treating variables as references, rather than values, often leads to simpler semantics, but requires extra indirection. Garbage collection, as previously noted in Section 8.5.3, dramatically eases the creation and maintenance of software, but imposes run-time costs. Dynamic method binding requires (in the general case) that methods be dispatched using vtables or some other lookup mechanism. Fully general implementations of multiple inheritance tend to impose overheads even when unused.
在一些情况下,我们还看到了时间/空间的权衡。如前文第 9.2.4 节所述,内联子程序可以显著提高包含许多小子程序的代码的性能,这不仅可以消除子程序调用本身的开销,还可以允许寄存器分配、公共子表达式分析和其他“全局”代码改进将应用于调用。同时,内联扩展通常会增加目标代码的大小。练习 C-10.28和C-10.30探讨了多重继承实现中的类似权衡。
In several cases we saw time/space tradeoffs as well. In-line subroutines, as previously noted in Section 9.2.4, can dramatically improve the performance of code with many small subroutines, not only by eliminating the overhead of the subroutine calls themselves, but by allowing register allocation, common subexpression analysis, and other “global” code improvements to be applied across calls. At the same time, in-line expansion generally increases the size of object code. Exercises C-10.28 and C-10.30 explore similar tradeoffs in the implementation of multiple inheritance.
从历史上看,Smalltalk 被广泛认为是最纯粹、最灵活的面向对象语言。然而,它缺乏编译时类型检查,加上其“基于消息”的计算模型以及对动态方法查找的需求,这往往使其实现速度相当慢。C++ 具有对象值变量、默认静态绑定、最少的动态检查和高质量的编译器,在很大程度上推动了 20 世纪 90 年代面向对象编程的普及。如今,对象无处不在 - 在静态类型、编译型语言(如 Java 和 C#)中;在动态类型语言(如 Python、Ruby、PHP 和 JavaScript)中;甚至在基于二进制组件或万维网上人类可读的服务调用的系统中(有关这些内容的更多信息,请参阅书目注释)。
Historically, Smalltalk was widely regarded as the purest and most flexible of the object-oriented languages. Its lack of compile-time type checking, however, together with its “message-based” model of computation and its need for dynamic method lookup, tended to make its implementations rather slow. C++, with its object-valued variables, default static binding, minimal dynamic checks, and high-quality compilers, was largely responsible for popularizing object-oriented programming in the 1990s. Today objects are ubiquitous—in statically typed, compiled languages like Java and C#; in dynamically typed languages like Python, Ruby, PHP, and JavaScript; and even in systems based on binary components or human-readable service invocations over the World Wide Web (more on these in the Bibliographic Notes).
10.1 一些语言设计者认为面向对象消除了对嵌套子程序的需求。你同意吗?为什么或为什么不?
10.1 Some language designers argue that object orientation eliminates the need for nested subroutines. Do you agree? Why or why not?
10.2设计一个类层次结构来表示 图 4.5中 CFG 的语法树。在每个类中提供一个方法来返回节点的值。提供充当make_leaf、make_un_op和make_bin_op子例程角色的构造函数。
10.2 Design a class hierarchy to represent syntax trees for the CFG of Figure 4.5. Provide a method in each class to return the value of a node. Provide constructors that play the role of the make_leaf, make_un_op, and make_bin_op subroutines.
10.3 重复上一个练习,但使用变体记录(联合)类型来表示语法树节点。使用类型扩展再次重复。从清晰度、抽象性、类型安全性和可扩展性方面比较这三种解决方案。
10.3 Repeat the previous exercise, but using a variant record (union) type to represent syntax tree nodes. Repeat again using type extensions. Compare the three solutions in terms of clarity, abstraction, type safety, and extensibility.
10.4 使用 C#索引器机制,创建一个可以像数组一样进行索引的哈希表类。(实际上,创建System.Collections.Hashtable容器类的简单版本。)或者,使用运算符 []的重载版本在 C++ 中构建类似的类。
10.4 Using the C# indexer mechanism, create a hash table class that can be indexed like an array. (In effect, create a simple version of the System.Collections.Hashtable container class.) Alternatively, use an overloaded version of operator[] to build a similar class in C++.
10.5本着 示例 10.8的精神,编写一个双端队列(deque)抽象(发音为“deck”),源自双向链表基类。
10.5 In the spirit of Example 10.8, write a double-ended queue (deque) abstraction (pronounced “deck”), derived from a doubly linked list base class.
10.6 使用模板(泛型)根据容器中的数据类型抽象出前两个问题的解决方案。
10.6 Use templates (generics) to abstract your solutions to the previous two questions over the type of data in the container.
10.7用 Python 或 Ruby 重复练习 10.5。编写一个简单的程序来演示泛型不需要抽象类型。如果在同一个双端队列中混合不同类型的对象会发生什么?
10.7 Repeat Exercise 10.5 in Python or Ruby. Write a simple program to demonstrate that generics are not needed to abstract over types. What happens if you mix objects of different types in the same deque?
10.8 使用示例 10.17中的列表类时,典型的 C++ 程序员将使用指针类型作为泛型参数V,以便list_nodes指向列表的元素。另一种实现方式是包含next和prev 在元素本身中为列表添加指针——通常是通过安排元素类型从类似示例 10.14中的gp_list_node类继承。结果有时被称为侵入式列表。
10.8 When using the list class of Example 10.17, the typical C++ programmer will use a pointer type for generic parameter V, so that list_nodes point to the elements of the list. An alternative implementation would include next and prev pointers for the list within the elements themselves—typically by arranging for the element type to inherit from something like the gp_list_node class of Example 10.14. The result is sometimes called an intrusive list.
(a) 解释如何在 C++ 中构建侵入式列表,而无需用户在其代码中加入显式类型转换。提示:给定多重继承,您可能需要确定每个具体元素类型在类型表示中 next和prev指针出现的偏移量。有关更多想法,请搜索有关流行 Boost 库的boost::intrusive::list类的信息。
(a) Explain how you might build intrusive lists in C++ without requiring users to pepper their code with explicit type casts. Hint: given multiple inheritance, you will probably need to determine, for each concrete element type, the offset within the representation of the type at which the next and prev pointers appear. For further ideas, search for information on the boost::intrusive::list class of the popular Boost library.
(b) 讨论侵入式和非侵入式列表的相对优点和缺点。
(b) Discuss the relative advantages and disadvantages of intrusive and non-intrusive lists.
10.9你能用 C# 或 C++ 模拟 示例 10.22的内部类吗?(提示:你需要 Java 对周围类的隐藏引用的显式版本。)
10.9 Can you emulate the inner class of Example 10.22 in C# or C++? (Hint: You'll need an explicit version of Java's hidden reference to the surrounding class.)
10.10为 图 10.2的列表抽象编写一个包主体。
10.10 Write a package body for the list abstraction of Figure 10.2.
10.11 用 Eiffel、Java 和/或 C# 重写列表和队列抽象。
10.11 Rewrite the list and queue abstractions in Eiffel, Java, and/or C#.
10.12 使用 C++、Java 或 C#,按照示例 10.25的精神实现一个Complex类。讨论在对象状态中保留所有四个值(x、y、ρ和θ)与仅保留两个值并根据需要计算其他值之间的时间和空间权衡。
10.12 Using C++, Java, or C#, implement a Complex class in the spirit of Example 10.25. Discuss the time and space tradeoffs between maintaining all four values (x, y, ρ, and θ) in the state of the object, or keeping only two and computing the others on demand.
10.13 对 Python 和/或 Ruby 重复前两个练习。
10.13 Repeat the previous two exercises for Python and/or Ruby.
10.14 比较 Java最终方法与 C++ 非虚拟方法。它们有何相同之处?有何不同之处?
10.14 Compare Java final methods with C++ nonvirtual methods. How are they the same? How are they different?
10.15 在一些面向对象语言中,包括 C++ 和 Eiffel,派生类可以隐藏基类的成员。例如,在 C++ 中,我们可以将基类声明为public、protected或private:class B:public A { … // A 的公共成员是 B 的公共成员// A 的受保护成员是 B 的受保护成员… class C:protected A { … // A 的公共和受保护成员是 C 的受保护成员… class D:private A { … // A 的公共和受保护成员是 D 的私有成员在所有情况下,B、C或D的方法都无法访问A的私有成员。考虑受保护和私有基类对动态方法绑定的影响。在什么情况下可以将对类B、C或D的对象的引用赋给类型A*的变量?
10.15 In several object-oriented languages, including C++ and Eiffel, a derived class can hide members of the base class. In C++, for example, we can declare a base class to be public, protected, or private:
class B : public A { …
// public members of A are public members of B
// protected members of A are protected members of B
…
class C : protected A { …
// public and protected members of A are protected members of C
…
class D : private A { …
// public and protected members of A are private members of D
In all cases, private members of A are inaccessible to methods of B, C, or D.
Consider the impact of protected and private base classes on dynamic method binding. Under what circumstances can a reference to an object of class B, C, or D be assigned into a variable of type A*?
10.16 如果我们重新定义数据成员,类的实现会发生什么?例如,假设我们有class foo { public: int a; char *b; }; … class bar : public foo { public: float c; int b; }; bar对象的表示包含一个b字段还是两个?如果是两个,是否可以访问,还是只能访问一个?在什么情况下?
10.16 What happens to the implementation of a class if we redefine a data member? For example, suppose we have
class foo {
public:
int a;
char *b;
};
…
class bar : public foo {
public:
float c;
int b;
};
Does the representation of a bar object contain one b field or two? If two, are both accessible, or only one? Under what circumstances?
10.17 讨论类和类型扩展的相对优点。你更喜欢哪一个?为什么?
10.17 Discuss the relative merits of classes and type extensions. Which do you prefer? Why?
10.18根据 示例 10.28的提纲,编写一个程序来说明 C++ 中复制构造函数和运算符=之间的区别。您的代码应包括可能调用这些函数的每种情况的示例(不要忘记参数传递和函数返回)。在每个类中检测复制构造函数和赋值运算符,以便它们在调用时打印其名称。运行您的程序以验证其行为是否符合您的预期。
10.18 Building on the outline of Example 10.28, write a program that illustrates the difference between copy constructors and operator= in C++. Your code should include examples of each situation in which one of these may be called (don't forget parameter passing and function returns). Instrument the copy constructors and assignment operators in each of your classes so that they will print their names when called. Run your program to verify that its behavior matches your expectations.
10.19 您如何看待 C++、C# 和 Ada 95 中默认使用静态方法绑定而不是动态方法绑定的决定?实现速度的提高是否值得以抽象和可重用性的损失为代价?假设我们有时需要静态绑定,您更喜欢 C++ 和 C# 的逐个方法方法,还是 Ada 95 的逐个变量方法?为什么?
10.19 What do you think of the decision, in C++, C#, and Ada 95, to use static method binding, rather than dynamic, by default? Is the gain in implementation speed worth the loss in abstraction and reusability? Assuming that we sometimes want static binding, do you prefer the method-by-method approach of C++ and C#, or the variable-by-variable approach of Ada 95? Why?
10.20 如果foo是 C++ 程序中的抽象类,为什么可以声明foo*类型的变量,但不能声明foo类型的变量?
10.20 If foo is an abstract class in a C++ program, why is it acceptable to declare variables of type foo*, but not of type foo?
10.21考虑 图 10.8所示的 Java 程序。假设该程序将在具有 4 字节地址的机器上编译为本机代码。
10.21 Consider the Java program shown in Figure 10.8. Assume that this is to be compiled to native code on a machine with 4-byte addresses.
(a) 画出第15行创建的对象在内存中的布局。显示所有虚函数表。
(a) Draw a picture of the layout in memory of the object created at line 15. Show all virtual function tables.
(二) 给出第 19 行调用c.val的汇编级伪代码。您可以假设c的地址在调用之前立即位于寄存器r1中,并且应该使用同一寄存器来传递隐藏的this参数。您可以忽略保存和恢复寄存器的需要,而不必担心将返回值放在哪里。
(b) Give assembly-level pseudocode for the call to c.val at line 19. You may assume that the address of c is in register r1 immediately before the call, and that this same register should be used to pass the hidden this parameter. You may ignore the need to save and restore registers, and don't worry about where to put the return value.
(c)给出第 17 行对 c.ping的调用的汇编级伪代码。同样假设c的地址在寄存器r1中,这是用于传递this 的同一寄存器,并且您不需要保存或恢复任何寄存器。
(c) Give assembly-level pseudocode for the call to c.ping at line 17. Again, assume that the address of c is in register r1, that this is the same register that should be used to pass this, and that you don't need to save or restore any registers.
(d)给出方法 Counter.ping主体的汇编级伪代码(再次忽略寄存器保存/恢复)。
(d) Give assembly-level pseudocode for the body of method Counter.ping (again ignoring register save/restore).
10.22 在 Ruby 中,与 Java 8 或 Scala 一样,接口(混合)可以提供方法代码以及签名。(它不能提供数据成员;否则会造成多重继承。)解释为什么动态类型使此功能比其他语言更强大。
10.22 In Ruby, as in Java 8 or Scala, an interface (mix-in) can provide method code as well as signatures. (It can't provide data members; that would be multiple inheritance.) Explain why dynamic typing makes this feature more powerful than it is in the other languages.
10.23–10.31 更深入。
10.23–10.31 In More Depth.
10.32 回到练习3.7 。构建图 3.16中单链表库的(更完整的)C++ 版本。讨论存储管理问题。在什么情况下删除列表本身时应该删除列表的元素?list_node的析构函数应该做什么?它应该删除其数据成员吗?它应该递归删除下一个节点吗?
10.32 Return for a moment to Exercise 3.7. Build a (more complete) C++ version of the singly linked list library of Figure 3.16. Discuss the issue of storage management. Under what circumstances should one delete the elements of a list when deleting the list itself? What should the destructor for list_node do? Should it delete its data member? Should it recursively delete node next?
10.33 本章的讨论集中在面向对象编程语言的经典“基于类”方法上,该方法由 Simula 和 Smalltalk 开创。还有一种替代方法,即“基于对象”方法,它摒弃了类的概念。在基于对象的编程中,方法直接与对象相关联,并使用现有对象作为原型来创建新对象。了解 Self(标准的基于对象的编程语言)和 JavaScript(使用最广泛的编程语言)。你如何看待它们的方法?它与基于类的替代方案相比如何?阅读第14.4.4 节中有关 JavaScript 的内容可能会对你有所帮助。
10.33 The discussion in this chapter has focused on the classic “class-based” approach to object-oriented programming languages, pioneered by Simula and Smalltalk. There is an alternative, “object-based” approach that dispenses with the notion of class. In object-based programming, methods are directly associated with objects, and new objects are created using existing objects as prototypes. Learn about Self, the canonical object-based programming language, and JavaScript, the most widely used. What do you think of their approach? How does it compare to the class-based alternative? You may find it helpful to read the coverage of JavaScript in Section 14.4.4.
10.34 如 C-5.5.1 节所述,流水线处理器的性能主要取决于硬件成功预测分支结果的能力,以便后续指令的处理可以在分支处理完成之前开始。然而,在面向对象程序中,仅仅知道分支结果是不够的:因为分支通常通过 vtable 进行分派,所以还必须预测目标。了解一个或多个现代处理器中分支预测的工作原理。这些处理器处理面向对象程序的效果如何?
10.34 As described in Section C-5.5.1, performance on pipelined processors depends critically on the ability of the hardware to successfully predict the outcome of branches, so that processing of subsequent instructions can begin before processing of the branch has completed. In object-oriented programs, however, knowing the outcome of a branch is not enough: because branches are so often dispatched through vtables, one must also predict the destination. Learn how branch prediction works in one or more modern processors. How well do these processors handle object-oriented programs?
10.35 探索即时(本机代码)Java 编译器中混合继承的实现。它是否遵循第 10.5 节的策略?它的效率如何?
10.35 Explore the implementation of mix-in inheritance in a just-in-time (native code) Java compiler. Does it follow the strategy of Section 10.5? How efficient is it?
10.36 探究 Ruby 中混合继承的实现。它与 Java 的实现有何不同?
10.36 Explore the implementation of mix-in inheritance in Ruby. How does it differ from that of Java?
10.37 了解类型层次分析和类型传播,它们有时可用于在编译时推断对象的具体类型,从而允许编译器生成对方法的直接调用,而不是通过 vtable 间接调用。这些技术有多有效?在典型的基准测试中,它们能够优化多少方法调用?它们的局限性是什么?(您可以从 Bacon 和 Sweeney [ BS96 ] 和 Diwan 等人 [ DMM96 ] 的论文开始。)
10.37 Learn about type hierarchy analysis and type propagation, which can sometimes be used to infer the concrete type of objects at compile time, allowing the compiler to generate direct calls to methods, rather than indirecting through vtables. How effective are these techniques? What fraction of method calls are they able to optimize in typical benchmarks? What are their limitations? (You might start with the papers of Bacon and Sweeney [BS96] and Diwan et al. [DMM96].)
10.38–10.39 更深入。
10.38–10.39 In More Depth.
附录 A包含本章讨论的各种语言的书目引用,包括 Simula、Smalltalk、C++、Eiffel、Java、C#、Modula-3、Oberon、Ada 95、Fortran 2003、Python、Ruby、Objective-C、Swift、Go、OCaml、和 CLOS。Lisp 的其他面向对象版本包括 Loops [ BS83 ] 和 Flavors [ Moo86 ]。
Appendix A contains bibliographic citations for the various languages discussed in this chapter, including Simula, Smalltalk, C++, Eiffel, Java, C#, Modula-3, Oberon, Ada 95, Fortran 2003, Python, Ruby, Objective-C, Swift, Go, OCaml, and CLOS. Other object-oriented versions of Lisp include Loops [BS83] and Flavors [Moo86].
Ellis 和 Stroustrup [ ES90 ] 对 C++ 历史版本的语义和实用问题进行了广泛的讨论。Stroustrup 文本 [ Str13 ] 的第三和第四部分全面概述了 C++ 中容器类的设计和实现。Deutsch 和 Schiffman [ DS84 ] 介绍了高效实现 Smalltalk 的技术。Borning 和 Ingalls [ BI82 ] 讨论了 Smalltalk-80 扩展中的多重继承。Strongtalk [ Sun06 ] 是 Sun Microsystems 于 20 世纪 90 年代开发的 Smalltalk 强类型后继者,后来作为开源发布。Gil 和 Sweeney [ GS99 ] 介绍了可用于降低多重继承的时间和空间复杂度的优化。
Ellis and Stroustrup [ES90] provide extensive discussion of both semantic and pragmatic issues for historic versions of C++. Parts III and IV of Stroustrup's text [Str13] provide a comprehensive survey of the design and implementation of container classes in C++. Deutsch and Schiffman [DS84] describe techniques to implement Smalltalk efficiently. Borning and Ingalls [BI82] discuss multiple inheritance in an extension to Smalltalk-80. Strongtalk [Sun06] is a strongly typed successor to Smalltalk developed at Sun Microsystems in the 1990s, and since released as open source. Gil and Sweeney [GS99] describe optimizations that can be used to reduce the time and space complexity of multiple inheritance.
Dolby [ Dol97 ] 描述了优化编译器如何识别嵌套对象可以扩展(在 Eiffel 意义上)的情况,同时保留引用语义。Bacon 和 Sweeney [ BS96 ] 以及 Diwan 等人 [ DMM96 ] 讨论了在编译时推断对象具体类型的技术,从而避免了 vtable 间接调用的开销。Driesen [ Dri93 ] 提出了一种 vtable 的替代方案,它需要进行全程序分析,但提供了极其高效的方法调度,即使在具有动态类型和多重继承的语言中也是如此。
Dolby [Dol97] describes how an optimizing compiler can identify circumstances in which a nested object can be expanded (in the Eiffel sense) while retaining reference semantics. Bacon and Sweeney [BS96] and Diwan et al. [DMM96] discuss techniques to infer the concrete type of objects at compile time, thereby avoiding the overhead of vtable indirection. Driesen [Dri93] presents an alternative to vtables that requires whole-program analysis, but provides extremely efficient method dispatch, even in languages with dynamic typing and multiple inheritance.
二进制组件系统允许将任意语言的任意编译器生成的代码组合成一个工作程序,通常跨越分布式机器集合。CORBA [ Sie00 ] 是由对象管理组织(一个由 700 多家公司组成的联盟)颁布的组件标准。.NET 是 Microsoft Corporation (microsoft.com/net) 的竞争标准,部分基于其早期的 ActiveX、DCOM 和 OLE [ Bro96 ] 产品。JavaBeans [ Sun97 ] 是用 Java 编写的符合 CORBA 的组件二进制标准。
Binary component systems allow code produced by arbitrary compilers for arbitrary languages to be joined together into a working program, often spanning a distributed collection of machines. CORBA [Sie00] is a component standard promulgated by the Object Management Group, a consortium of over 700 companies. .NET is a competing standard from Microsoft Corporation (microsoft.com/net), based in part on their earlier ActiveX, DCOM, and OLE [Bro96] products. JavaBeans [Sun97] is a CORBA-compliant binary standard for components written in Java.
随着 Web 服务的激增,分布式系统已被设计为以人类可读的形式交换和操作对象。SOAP [ Wor12 ] 最初是“简单对象访问协议”的缩写,是基于 Web 的信息传输和方法调用的标准。其底层数据通常编码为 XML(可扩展标记语言)[ Wor06a ]。近年来,SOAP 已基本被 REST(表述性状态转移)[ Fie00 ] 所取代,REST 是一套更为非正式的约定,位于普通 HTTP 之上。REST 中的底层数据可能采用多种形式 - 最常见的是 JSON(JavaScript 对象表示法)[ ECM13 ]。
With the explosion of web services, distributed systems have been designed to exchange and manipulate objects in human-readable form. SOAP [Wor12], originally an acronym for Simple Object Access Protocol, is a standard for web-based information transfer and method invocation. Its underlying data is typically encoded as XML (extensible markup language) [Wor06a]. In recent years, SOAP has largely been supplanted by REST (Representational State Transfer) [Fie00], a more informal set of conventions layered on top of ordinary HTTP. The underlying data in REST may take a variety of forms—most commonly JSON (JavaScript Object Notation) [ECM13].
面向对象编程领域的许多开创性论文都发表在ACM OOPSLA会议(面向对象编程系统、语言和应用程序)的论文集中,该会议自 1986 年以来每年举办一次,并作为ACM SIGPLAN 通知的特刊发表。Wegner [ Weg90 ] 列举了面向对象的定义特征。Meyer [ Mey92b,第 21.10 节] 解释了动态方法绑定的基本原理。Ungar 和 Smith [ US91 ] 描述了 Self,一种规范的基于对象(而不是基于类)的语言。
Many of the seminal papers in object-oriented programming have appeared in the proceedings of the ACM OOPSLA conferences (Object-Oriented Programming Systems, Languages, and Applications), held annually since 1986, and published as special issues of ACM SIGPLAN Notices. Wegner [Weg90] enumerates the defining characteristics of object orientation. Meyer [Mey92b, Sec. 21.10] explains the rationale for dynamic method binding. Ungar and Smith [US91] describe Self, the canonical object-based (as opposed to class-based) language.
替代编程模型
Alternative Programming Models
正如我们在第 1 章中提到的,编程语言传统上分为各种命令式和声明式语言,尽管这种分类并不完美。我们在第一部分和第二部分中曾提到过对每个主要语言系列特别重要的问题。此外,我们所涵盖的大部分内容(语法、语义、命名、类型、抽象)都适用于所有语言。不过,我们的注意力主要集中在主流命令式语言上。在第三部分中,我们将转移这一焦点。
As we noted in Chapter 1, programming languages are traditionally though imperfectly classified into various imperative and declarative families. We have had occasion in Parts I and II to mention issues of particular importance to each of the major families. Moreover much of what we have covered—syntax, semantics, naming, types, abstraction—applies uniformly to all. Still, our attention has focused mostly on mainstream imperative languages. In Part III we shift this focus.
函数式和逻辑语言是主要的非命令式选项。我们分别在第 11 章和第12章中讨论它们。在每种情况下,我们都围绕代表性语言进行讨论:用于函数式编程的 Scheme 和 OCaml,用于逻辑编程的 Prolog。在第 11 章中,我们还介绍了急切求值和惰性求值,以及一等函数和高阶函数。在第12 章中,我们讨论了导致全自动通用逻辑编程困难的问题,并描述了在实践中用来保持模型易于处理的限制。这两章中的可选部分都考虑了数学基础:用于函数式编程的 Lambda 演算,用于逻辑编程的谓词演算。
Functional and logic languages are the principal nonimperative options. We consider them in Chapters 11 and 12, respectively. In each case we structure our discussion around representative languages: Scheme and OCaml for functional programming, Prolog for logic programming. In Chapter 11 we also cover eager and lazy evaluation, and first-class and higher-order functions. In Chapter 12 we cover issues that make fully automatic, general purpose logic programming difficult, and describe restrictions used in practice to keep the model tractable. Optional sections in both chapters consider mathematical foundations: Lambda Calculus for functional programming, Predicate Calculus for logic programming.
其余两章讨论了并发和脚本模型,这两种模型都越来越流行,并且跨越了命令式/声明式的界限。并发是由互联计算机的硬件并行性以及即将到来的多线程处理器和芯片级多处理器的爆炸式增长所驱动的。脚本是由万维网的增长和对程序员生产力的日益重视所驱动的,这种重视将快速开发和可重用性置于纯粹的运行时性能之上。
The remaining two chapters consider concurrent and scripting models, both of which are increasingly popular, and cut across the imperative/declarative divide. Concurrency is driven by the hardware parallelism of internetworked computers and by the coming explosion in multithreaded processors and chip-level multiprocessors. Scripting is driven by the growth of the World Wide Web and by an increasing emphasis on programmer productivity, which places rapid development and reusability above sheer run-time performance.
第 13 章从并发基础知识开始,包括通信和同步、线程创建语法以及线程的实现。本章的其余部分分为共享内存模型(其中线程使用显式或隐式同步机制来管理一组通用变量)和(在配套网站上)消息传递模型(其中线程仅通过显式通信进行交互)。
Chapter 13 begins with the fundamentals of concurrency, including communication and synchronization, thread creation syntax, and the implementation of threads. The remainder of the chapter is divided between shared-memory models, in which threads use explicit or implicit synchronization mechanisms to manage a common set of variables, and (on the companion site) message-passing models, in which threads interact only through explicit communication.
第 14 章的前半部分概述了脚本在其中发挥重要作用的问题领域:shell(命令)语言、文本处理和报告生成、数学和统计、程序组件的“粘合”、复杂应用程序的扩展机制以及客户端和服务器端 Web 脚本。后半部分讨论了脚本语言所倡导的一些更重要的语言创新:灵活的作用域和命名约定、字符串和模式操作(扩展正则表达式)以及高级数据类型。
The first half of Chapter 14 surveys problem domains in which scripting plays a major role: shell (command) languages, text processing and report generation, mathematics and statistics, the "gluing" together of program components, extension mechanisms for complex applications, and client and server-side Web scripting. The second half considers some of the more important language innovations championed by scripting languages: flexible scoping and naming conventions, string and pattern manipulation (extended regular expressions), and high level data types.
本书前几章主要关注命令式编程语言。在本章和下一章中,我们将重点介绍函数式和逻辑语言。虽然命令式语言的使用范围更广,但函数式和逻辑语言都有“工业强度”的实现,并且这两种模型都有重要的商业应用。Lisp 传统上因处理符号数据而广受欢迎,尤其是在人工智能领域。OCaml 在金融服务行业得到广泛使用。近年来,函数式语言(尤其是静态类型的语言)在科学应用中也越来越受欢迎。逻辑语言广泛用于形式化规范和定理证明,还用于许多其他应用,但使用范围较窄。
Previous chapters of this text have focused largely on imperative programming languages. In the current chapter and the next we emphasize functional and logic languages instead. While imperative languages are far more widely used, “industrial-strength” implementations exist for both functional and logic languages, and both models have commercially important applications. Lisp has traditionally been popular for the manipulation of symbolic data, particularly in the field of artificial intelligence. OCaml is heavily used in the financial services industry. In recent years functional languages—statically typed ones in particular—have become increasingly popular for scientific applications as well. Logic languages are widely used for formal specifications and theorem proving and, less widely, for many other applications.
当然,函数式语言和逻辑语言与命令式语言有很多共同之处。每个模型下都会出现命名和范围问题。类型、表达式以及选择和递归的控制流概念也是如此。所有语言都必须进行语义扫描、解析和分析。此外,函数式语言大量使用子程序——甚至比大多数冯·诺依曼语言还要多——并且并发性和不确定性的概念在函数式语言和逻辑语言中与在命令式语言中一样常见。
Of course, functional and logic languages have a great deal in common with their imperative cousins. Naming and scoping issues arise under every model. So do types, expressions, and the control-flow concepts of selection and recursion. All languages must be scanned, parsed, and analyzed semantically. In addition, functional languages make heavy use of subroutines—more so even than most von Neumann languages—and the notions of concurrency and nondeterminacy are as common in functional and logic languages as they are in the imperative case.
如第 1 章所述,语言类别之间的界限往往相当模糊。人们可以在许多命令式语言中以大量函数式风格编写代码,许多函数式语言都包含命令式特性(赋值和迭代)。最常见的逻辑语言 Prolog 也提供了某些命令式特性。最后,在大多数函数式编程语言中,构建逻辑编程系统都很容易。
As noted in Chapter 1, the boundaries between language categories tend to be rather fuzzy. One can write in a largely functional style in many imperative languages, and many functional languages include imperative features (assignment and iteration). The most common logic language—Prolog—provides certain imperative features as well. Finally, it is easy to build a logic programming system in most functional programming languages.
由于命令式和函数式概念的重叠,我们在前几章中曾多次讨论过对函数式编程语言特别重要的问题。大多数此类语言严重依赖多态性(隐式参数类型 -第 7.1.2 节、第 7.3 节和第 7.2.4 节)。大多数语言大量使用列表(第 8.6 节)。从历史上看,有几个是动态范围的(第 3.3.6 节和 C-3.4.2 节)。所有语言都使用递归(第 6.6 节)进行重复执行,结果程序行为和性能在很大程度上取决于参数的评估规则(第 6.6.2 节)。所有这些都倾向于生成大量临时数据,其实现通过垃圾收集来回收这些数据(第 8.5.3 节)。
Because of the overlap between imperative and functional concepts, we have had occasion several times in previous chapters to consider issues of particular importance to functional programming languages. Most such languages depend heavily on polymorphism (the implicit parametric kind—Sections 7.1.2, 7.3, and 7.2.4). Most make heavy use of lists (Section 8.6). Several, historically, were dynamically scoped (Sections 3.3.6 and C-3.4.2). All employ recursion (Section 6.6) for repetitive execution, with the result that program behavior and performance depend heavily on the evaluation rules for parameters (Section 6.6.2). All have a tendency to generate significant amounts of temporary data, which their implementations reclaim through garbage collection (Section 8.5.3).
本章首先简要介绍命令式、函数式和逻辑编程模型的历史起源。然后,我们列举函数式编程的基本概念,并考虑如何在 Lisp 的 Scheme 方言和 ML 的 OCaml 方言中实现这些概念。简而言之,我们还考虑了 Common Lisp、Erlang、Haskell、Miranda、pH、单一赋值 C 和 Sisal。我们特别关注求值顺序和高阶函数的问题。对于那些对函数式编程的理论基础感兴趣的人,我们(在配套网站上)提供了函数、集合和 lambda 演算的介绍。形式主义有助于阐明纯函数式语言的概念,并阐明了实际语言与数学抽象的不同之处。
Our chapter begins with a brief introduction to the historical origins of the imperative, functional, and logic programming models. We then enumerate fundamental concepts in functional programming and consider how these are realized in the Scheme dialect of Lisp and the OCaml dialect of ML. More briefly, we also consider Common Lisp, Erlang, Haskell, Miranda, pH, Single Assignment C, and Sisal. We pay particular attention to issues of evaluation order and higher-order functions. For those with an interest in the theoretical foundations of functional programming, we provide (on the companion site) an introduction to functions, sets, and the lambda calculus. The formalism helps to clarify the notion of a pure functional language, and illuminates the places where practical languages diverge from the mathematical abstraction.
要理解不同编程模型之间的差异,了解它们的理论根源会有所帮助,因为它们都早于电子计算机的发展。命令式和函数式模型源自数学家阿兰·图灵、阿隆佐·丘奇、斯蒂芬·克莱恩、埃米尔·波斯特等人在 20 世纪 30 年代所做的工作。这些人基本上独立工作,基于自动机、符号运算、递归函数定义和组合学,开发了几种非常不同的算法或有效程序概念的形式化。随着时间的推移,这些不同的形式化被证明同样强大:任何可以在一种形式中计算的东西都可以在其他形式中计算。这个结果让丘奇猜想,任何直观吸引人的计算模型也同样强大;这个猜想被称为丘奇论点。
To understand the differences among programming models, it can be helpful to consider their theoretical roots, all of which predate the development of electronic computers. The imperative and functional models grew out of work undertaken by mathematicians Alan Turing, Alonzo Church, Stephen Kleene, Emil Post, and others in the 1930s. Working largely independently, these individuals developed several very different formalizations of the notion of an algorithm, or effective procedure, based on automata, symbolic manipulation, recursive function definitions, and combinatorics. Over time, these various formalizations were shown to be equally powerful: anything that could be computed in one could be computed in the others. This result led Church to conjecture that any intuitively appealing model of computing would be equally powerful as well; this conjecture is known as Church's thesis.
图灵的计算模型是图灵机,这是一种让人联想到有限或下推自动机的自动机,但能够访问无限存储“磁带”的任意单元。1图灵机以命令式方式计算,通过更改磁带单元中的值,就像高级命令式程序通过更改变量的值进行计算一样。丘奇的计算模型称为 lambda 演算。它基于参数化表达式的概念(每个参数由表达式的出现引入)。字母 λ——因此得名)。2 Lambda演算启发了函数式编程:人们使用它通过将参数代入表达式来进行计算,就像在高级函数式程序中通过将参数传递给函数来进行计算一样。Kleene 和 Post 的计算模型更加抽象,不适合直接作为编程语言实现。
Turing's model of computing was the Turing machine, an automaton reminiscent of a finite or pushdown automaton, but with the ability to access arbitrary cells of an unbounded storage “tape.”1 The Turing machine computes in an imperative way, by changing the values in cells of its tape, just as a high-level imperative program computes by changing the values of variables. Church's model of computing is called the lambda calculus. It is based on the notion of parameterized expressions (with each parameter introduced by an occurrence of the letter λ—hence the notation's name).2 Lambda calculus was the inspiration for functional programming: one uses it to compute by substituting parameters into expressions, just as one computes in a high-level functional program by passing arguments to functions. The computing models of Kleene and Post are more abstract, and do not lend themselves directly to implementation as a programming language.
可计算性早期研究的目标不是理解计算机(除了纯机械设备外,计算机并不存在),而是将有效程序的概念形式化。随着时间的推移,这项工作使数学家能够形式化构造性证明(展示如何获得具有某些所需属性的数学对象的证明)和非构造性证明(仅展示这样的对象必须存在,可能是通过矛盾、计数论证或归结为其他非构造性证明的定理)之间的区别。实际上,程序可以看作是以下命题的构造性证明:给定任何适当的输入,存在以特定期望方式与输入相关的输出。例如,欧几里得算法可以被认为是以下命题的构造性证明:每对非负整数都有一个最大公约数。
The goal of early work in computability was not to understand computers (aside from purely mechanical devices, computers did not exist) but rather to formalize the notion of an effective procedure. Over time, this work allowed mathematicians to formalize the distinction between a constructive proof (one that shows how to obtain a mathematical object with some desired property) and a nonconstructive proof (one that merely shows that such an object must exist, perhaps by contradiction, or counting arguments, or reduction to some other theorem whose proof is nonconstructive). In effect, a program can be seen as a constructive proof of the proposition that, given any appropriate inputs, there exist outputs that are related to the inputs in a particular, desired way. Euclid's algorithm, for example, can be thought of as a constructive proof of the proposition that every pair of non-negative integers has a greatest common divisor.
逻辑编程也与构造性证明的概念密切相关,但处于更抽象的层次。逻辑程序员不会编写适用于所有适当输入的通用构造性证明,而是编写一组公理,让计算机能够为每组特定的输入找到构造性证明。我们将在第 12 章中更详细地讨论逻辑编程。
Logic programming is also intimately tied to the notion of constructive proofs, but at a more abstract level. Rather than write a general constructive proof that works for all appropriate inputs, the logic programmer writes a set of axioms that allow the computer to discover a constructive proof for each particular set of inputs. We will consider logic programming in more detail in Chapter 12.
从严格意义上讲,函数式编程将程序的输出定义为输入的数学函数,没有内部状态的概念,因此没有副作用。在我们这里考虑的语言中,Miranda、Haskell、pH、Sisal 和 Single Assignment C 是纯函数式的。Erlang 几乎如此。大多数其他语言都包含命令式特性。为了使函数式编程实用,函数式语言提供了许多命令式语言中经常缺少的特性,包括
In a strict sense of the term, functional programming defines the outputs of a program as a mathematical function of the inputs, with no notion of internal state, and thus no side effects. Among the languages we consider here, Miranda, Haskell, pH, Sisal, and Single Assignment C are purely functional. Erlang is nearly so. Most others include imperative features. To make functional programming practical, functional languages provide a number of features that are often missing in imperative languages, including
■ First-class function values and higher-order functions
■ 广泛的多态性
■ Extensive polymorphism
■ 列表类型和运算符
■ List types and operators
■ 结构化函数返回
■ Structured function returns
■ 结构化对象的构造函数(聚合)
■ Constructors (aggregates) for structured objects
■ 垃圾收集
■ Garbage collection
在第 3.6.2 节中,我们将一等值定义为可以作为参数传递、从子程序返回或(在具有副作用的语言中)赋给变量的值。严格解释该术语,一等状态还需要能够在运行时创建(计算)新值。对于子程序,这种一等状态的概念需要嵌套的lambda 表达式,这些表达式可以捕获在周围范围内定义的值,从而为这些值提供无限范围(即,即使其范围不再有效,它们仍保持活动状态)。子程序在大多数命令式语言中是二等值,但在所有函数式编程语言中都是一等值(严格意义上来说)。高阶函数将函数作为参数,或返回函数作为结果。
In Section 3.6.2 we defined a first-class value as one that can be passed as a parameter, returned from a subroutine, or (in a language with side effects) assigned into a variable. Under a strict interpretation of the term, first-class status also requires the ability to create (compute) new values at run time. In the case of subroutines, this notion of first-class status requires nested lambda expressions that can capture values defined in surrounding scopes, giving those values unlimited extent (i.e., keeping them alive even after their scopes are no longer active). Subroutines are second-class values in most imperative languages, but first-class values (in the strict sense of the term) in all functional programming languages. A higher-order function takes a function as an argument, or returns a function as a result.
多态性在函数式语言中很重要,因为它允许将函数用于尽可能通用的一类参数。正如我们在7.1和7.2.4节中看到的,Lisp 及其方言是动态类型的,因此本质上是多态的,而 ML 及其相关语言则通过类型推断机制获得多态性。列表在函数式语言中很重要,因为它们具有自然的递归定义,并且可以通过操作其第一个元素并(递归地)操作列表的其余部分来轻松操作。递归很重要,因为在没有副作用的情况下,它是重复做任何事情的唯一方法。
Polymorphism is important in functional languages because it allows a function to be used on as general a class of arguments as possible. As we have seen in Sections 7.1 and 7.2.4, Lisp and its dialects are dynamically typed, and thus inherently polymorphic, while ML and its relatives obtain polymorphism through the mechanism of type inference. Lists are important in functional languages because they have a natural recursive definition, and are easily manipulated by operating on their first element and (recursively) the remainder of the list. Recursion is important because in the absence of side effects it provides the only means of doing anything repeatedly.
我们列出的函数式语言特性(递归、结构化函数返回、构造函数、垃圾收集)中的几项可以在部分(但不是全部)命令式语言中找到。Fortran 77 没有递归,也不允许从函数返回结构化类型(即数组)。Pascal 和早期版本的 Modula-2 只允许从函数返回简单类型和指针类型。正如我们在第 7.1.3 节中看到的,包括 Ada、C 和 Fortran 90 在内的几种命令式语言提供了允许在行内指定结构化值的聚合构造。然而,在大多数命令式语言中,这种构造是缺乏的或不完整的。C# 和几种脚本语言(其中包括 Python 和 Ruby)提供了能够表示(未命名的)函数值(lambda 表达式)的聚合,但很少有命令式语言具有如此丰富的表现力。纯函数式语言必须提供完全通用的聚合:因为没有办法更新现有对象,所以必须“一次性”初始化新创建的对象。最后,尽管垃圾收集在命令式语言中越来越常见,但它绝不是通用的,通常也不适用于子例程的局部变量,这些局部变量通常在堆栈中分配。由于希望为第一类函数和其他对象提供无限范围,函数式语言倾向于对所有动态分配的数据(或至少对所有编译器无法证明堆栈分配是安全的数据)采用(垃圾收集)堆。C++11 和 Java 8 提供 lambda 表达式,但没有无限范围。
Several of the items in our list of functional language features (recursion, structured function returns, constructors, garbage collection) can be found in some but not all imperative languages. Fortran 77 has no recursion, nor does it allow structured types (i.e., arrays) to be returned from functions. Pascal and early versions of Modula-2 allow only simple and pointer types to be returned from functions. As we saw in Section 7.1.3, several imperative languages, including Ada, C, and Fortran 90, provide aggregate constructs that allow a structured value to be specified in-line. In most imperative languages, however, such constructs are lacking or incomplete. C# and several scripting languages—Python and Ruby among them—provide aggregates capable of representing an (unnamed) functional value (a lambda expression), but few imperative languages are so expressive. A pure functional language must provide completely general aggregates: because there is no way to update existing objects, newly created ones must be initialized “all at once.” Finally, though garbage collection is increasingly common in imperative languages, it is by no means universal, nor does it usually apply to the local variables of subroutines, which are typically allocated in the stack. Because of the desire to provide unlimited extent for first-class functions and other objects, functional languages tend to employ a (garbage-collected) heap for all dynamically allocated data (or at least for all data for which the compiler is unable to prove that stack allocation is safe). C++11 and Java 8 provide lambda expressions, but without unlimited extent.
由于 Lisp 是最初的函数式语言,并且仍然是使用最广泛的语言之一,因此人们通常将 Lisp 的几个特性描述为与一般的函数式编程有关,尽管这种描述并不准确。我们将在第11.3 节中(在 Scheme 的上下文中)研究这些特性。它们包括
Because Lisp was the original functional language, and is still one of the most widely used, several characteristics of Lisp are commonly, though inaccurately, described as though they pertained to functional programming in general. We will examine these characteristics (in the context of Scheme) in Section 11.3. They include
■ 程序和数据的同质性:Lisp 中的程序本身就是一个列表,可以用操作数据的相同机制进行操作。
■ Homogeneity of programs and data: A program in Lisp is itself a list, and can be manipulated with the same mechanisms used to manipulate data.
■ 自定义:Lisp 的操作语义可以通过用Lisp 编写的解释器来优雅地定义。
■ Self-definition: The operational semantics of Lisp can be defined elegantly in terms of an interpreter written in Lisp.
■ 通过“读取-评估-打印”循环与用户交互。
■ Interaction with the user through a “read-eval-print” loop.
许多程序员(可能是大多数程序员)都用命令式和函数式风格编写了大量软件,他们发现后者更美观。此外,各种大型商业项目的经验(参见本章末尾的参考书目注释)表明,没有副作用使得函数式程序比命令式程序更容易编写、调试和维护。当传递一组给定的参数时,纯函数总是可以返回相同的结果。未记录的副作用、错误顺序的更新以及悬空或(在大多数情况下)未初始化的引用等问题根本不会发生。同时,许多函数式语言的实现在可移植性、库包的丰富性、与其他语言的接口以及调试和分析工具方面仍然存在不足。我们将在第11.8 节中回到函数式和命令式编程之间的权衡。
Many programmers—probably most—who have written significant amounts of software in both imperative and functional styles find the latter more aesthetically appealing. Moreover, experience with a variety of large commercial projects (see the Bibliographic Notes at the end of the chapter) suggests that the absence of side effects makes functional programs significantly easier to write, debug, and maintain than their imperative counterparts. When passed a given set of arguments, a pure function can always be counted on to return the same results. Issues of undocumented side effects, misordered updates, and dangling or (in most cases) uninitialized references simply don't occur. At the same time, many implementations of functional languages still fall short in terms of portability, richness of library packages, interfaces to other languages, and debugging and profiling tools. We will return to the tradeoffs between functional and imperative programming in Section 11.8.
Scheme 最初由 Guy Steele 和 Gerald Sussman 于 20 世纪 70 年代末开发,并经过多次修订。此处的描述遵循 1998 年 R5RS(第五次修订标准),也应符合 2013 年 R7RS。
Scheme was originally developed by Guy Steele and Gerald Sussman in the late 1970s, and has evolved through several revisions. The description here follows the 1998 R5RS (fifth revised standard), and should also be compliant with the 2013 R7RS.
Lambda表达式不为其函数命名;这可以使用let或define来完成(将在下一小节中介绍)。从这个意义上讲,lambda表达式类似于我们在7.1.3 节中用来指定数组或记录值的聚合。
A lambda expression does not give its function a name; this can be done using let or define (to be introduced in the next subsection). In this sense, a lambda expression is like the aggregates that we used in Section 7.1.3 to specify array or record values.
为了快速访问序列中的任意元素,Scheme 提供了一种向量类型,该类型由整数索引,就像数组一样,并且可以包含异构类型的元素,就像记录一样。感兴趣的读者可以参阅 Scheme 手册 [ SDF + 07 ] 以了解更多信息。
For fast access to arbitrary elements of a sequence, Scheme provides a vector type that is indexed by integers, like an array, and may have elements of heterogeneous types, like a record. Interested readers are referred to the Scheme manual [SDF+07] for further information.
Scheme 还提供了大量的数值和逻辑(布尔)函数和特殊形式。语言手册描述了五种数值类型的层次结构:整数、有理数、实数、复数和数字。最后两个级别是可选的:实现可以选择不提供任何非实数。大多数但并非所有实现都采用整数和有理数的任意精度表示,后者在内部存储为(分子,分母)对。
Scheme also provides a wealth of numeric and logical (Boolean) functions and special forms. The language manual describes a hierarchy of five numeric types: integer, rational, real, complex, and number. The last two levels are optional: implementations may choose not to provide any numbers that are not real. Most but not all implementations employ arbitrary-precision representations of both integers and rationals, with the latter stored internally as (numerator, denominator) pairs.
Scheme 提供了几种不同的相等性测试函数。对于数值比较,=在必要时执行类型转换(例如,比较整数和浮点数)。对于一般用途,eqv?执行浅比较,而equal?执行深度(递归)比较,在叶子节点处使用eqv? 。eq?函数也执行浅比较,在某些情况下可能比eqv?更便宜(特别是,eq?不需要检测存储在不同位置的离散值的相等性,尽管在某些实现中可能需要)。第 7.4 节介绍了更多详细信息。
Scheme provides several different equality-testing functions. For numerical comparisons, = performs type conversions where necessary (e.g., to compare an integer and a floating-point number). For general-purpose use, eqv? performs a shallow comparison, while equal? performs a deep (recursive) comparison, using eqv? at the leaves. The eq? function also performs a shallow comparison, and may be cheaper than eqv? in certain circumstances (in particular, eq? is not required to detect the equality of discrete values stored in different locations, though it may in some implementations). Further details were presented in Section 7.4.
当然,递归是 Scheme 中重复执行操作的主要方法。第 6.6 节讨论了许多与递归相关的问题;我们在此不再重复讨论。
Recursion, of course, is the principal means of doing things repeatedly in Scheme. Many issues related to recursion were discussed in Section 6.6; we do not repeat that discussion here.
前面的章节中提到了另外两种控制流构造。延迟和强制(第 6.6.2 节)允许对表达式进行惰性求值。使用当前继续调用(call/cc;第 6.2.2 节)允许以闭包的形式保存当前程序计数器和引用环境,并将其传递给指定的子例程。我们将在第 11.5 节中再次提到延迟和强制。
Two other control-flow constructs have been mentioned in previous chapters. Delay and force (Section 6.6.2) permit the lazy evaluation of expressions. Call-with-current-continuation (call/cc; Section 6.2.2) allows the current program counter and referencing environment to be saved in the form of a closure, and passed to a specified subroutine. We will mention delay and force again in Section 11.5.
现在应该清楚了,Scheme 中的程序采用列表的形式。从技术术语上讲,我们说 Lisp 和 Scheme 是同像的— 自我表示。无论我们将其视为程序还是列表,带括号的符号字符串(其中括号是匹配的)都称为S 表达式。事实上,未求值的程序是一个列表,可以使用所有常用的列表函数构造、解构和以其他方式操作。
As should be clear by now, a program in Scheme takes the form of a list. In technical terms, we say that Lisp and Scheme are homoiconic—self-representing. A parenthesized string of symbols (in which parentheses are balanced) is called an S-expression regardless of whether we think of it as a program or as a list. In fact, an unevaluated program is a list, and can be constructed, deconstructed, and otherwise manipulated with all the usual list functions.
Lisp [ MAE + 65 ]的原始描述包括该语言的自我定义:用 Lisp 编写的 Lisp 解释器代码。尽管 Scheme 在很多方面都与早期的 Lisp 不同(最明显的是其对词法作用域的使用),但仍然可以轻松编写这样的元循环解释器 [ AS96,第 4 章]。代码基于函数eval和apply。其中第一个我们刚刚看到过。第二个,apply,接受两个参数:一个函数和一个列表。它实现了调用函数的效果,并使用列表的元素作为参数。
The original description of Lisp [MAE+65] included a self-definition of the language: code for a Lisp interpreter, written in Lisp. Though Scheme differs in many ways from this early Lisp (most notably in its use of lexical scoping), such a metacircular interpreter can still be written easily [AS96, Chap. 4]. The code is based on the functions eval and apply. The first of these we have just seen. The second, apply, takes two arguments: a function and a list. It achieves the effect of calling the function, with the elements of the list as arguments.
和 Lisp 一样,ML 也有着复杂的家族史。最初的语言是由 Robin Milner 和剑桥大学的其他人在 20 世纪 70 年代初发明的。SML(“标准” ML)和 OCaml(Objective Caml)是当今使用最广泛的两种方言。Haskell 是函数式编程研究中使用最广泛的语言,它是 ML 的一个独立后代(通过 Miranda)。由微软和其他公司开发的 F# 是 OCaml 的后代。
Like Lisp, ML has a complicated family tree. The original language was devised in the early 1970s by Robin Milner and others at Cambridge University. SML (“Standard” ML) and OCaml (Objective Caml) are the two most widely used dialects today. Haskell, the most widely used language for functional programming research, is a separate descendant of ML (by way of Miranda). F#, developed by Microsoft and others, is a descendant of OCaml.
自 20 世纪 80 年代初以来,法国国家计算研究机构 INRIA 的研究人员一直领导着 OCaml(及其前身 Caml)的开发工作(“O”是在名称中添加的,以引入面向对象特性)在 20 世纪 90 年代初)。在 ML 家族语言中,OCaml 以 INRIA 实现的效率和广泛的商业应用而闻名:在其他领域中,OCaml 在金融行业很受欢迎。
Work on OCaml (and its predecessor, Caml) has been led since the early 1980s by researchers at INRIA, the French national computing research organization (the 'O' was added to the name with the introduction of object-oriented features in the early 1990s). Among the ML family languages, OCaml is known for the efficiency of the INRIA implementation and for its widespread commercial adoption: among other domains, OCaml is popular in the finance industry.
INRIA OCaml 发行版包含字节码编译器(附带虚拟机)和针对各种机器架构优化的本机代码编译器。解释器可以交互使用,也可以执行以前编写的程序。学习该语言的最简单方法是交互地试用解释器。本节其余部分中的示例均在该环境中运行。
The INRIA OCaml distribution includes both a byte-code compiler (with accompanying virtual machine) and an optimizing native-code compiler for a variety of machine architectures. The interpreter can be used either interactively or to execute a previously written program. The easiest way to learn the language is to experiment with the interpreter interactively. The examples in the remainder of this section all work in that environment.
OCaml 中的词汇约定很简单:标识符由大小写字母、数字、下划线和单引号组成;大多数标识符必须以小写字母或下划线开头(少数特殊名称,包括类型构造函数、变体、模块和异常,必须以大写字母开头)。注释以 ( * … * ) 分隔,并允许嵌套。浮点数必须包含小数点:表达式cos 0将生成类型冲突错误消息。
Lexical conventions in OCaml are straightforward: Identifiers are composed of upper- and lower-case letters, digits, underscores, and single quote marks; most are required to start with a lower-case letter or underscore (a few special kinds of names, including type constructors, variants, modules, and exceptions, must start with an upper-case letter). Comments are delimited with (* … *), and are permitted to nest. Floating-point numbers are required to contain a decimal point: the expression cos 0 will generate a type-clash error message.
内置类型包括布尔值、整数、浮点数、字符和字符串。可以使用各种类型构造函数创建更复杂类型的值,包括列表、数组、元组、记录、变体、对象和类;其中几种在11.4.3 节中描述。如7.2.4 节所述,类型检查是通过推断每个表达式的类型来执行的,然后检查每当两个表达式需要是同一类型时(例如,因为一个是参数,另一个是相应的形式参数),推断结果是否相同。为了支持类型推断,一些在其他语言中重载的运算符在 OCaml 中是分开的。特别是,通常的算术运算有整数(+、−、*、/)和浮点数(+.、−.、*.、/.)版本。
Built-in types include Boolean values, integers, floating-point numbers, characters, and strings. Values of more complex types can be created using a variety of type constructors, including lists, arrays, tuples, records, variants, objects, and classes; several of these are described in Section 11.4.3. As discussed in Section 7.2.4, type checking is performed by inferring a type for every expression, and then checking that whenever two expressions need to be of the same type (e.g., because one is an argument and the other is the corresponding formal parameter), the inferences turn out to be the same. To support type inference, some operators that are overloaded in other languages are separate in OCaml. In particular, the usual arithmetic operations have both integer (+, −, *, /) and floating-point (+., −., *., /.) versions.
排序比较(<、>、<=、>=)始终基于深度比较。它在 OCaml 中针对函数以外的所有类型都进行了定义。它对算术类型、字符和字符串(后者按字典顺序工作)执行通常预期的操作;对于其他类型,结果是确定性的但不一定直观。在所有情况下,结果都与结构相等性测试(=)一致:如果a = b,则a <= b且a >= b;如果a <> b,则a < b或a > b。与相等性测试一样,函数比较将导致运行时异常;循环结构的比较可能不会终止。
Comparison for ordering (<, >, <=, >=) is always based on deep comparison. It is defined in OCaml on all types other than functions. It does what one would normally expect on arithmetic types, characters, and strings (the latter works lexicographically); on other types the results are deterministic but not necessarily intuitive. In all cases, the results are consistent with the structural equality test (=): if a = b, then a <= b and a >= b; if a <> b, then a < b or a > b. As with the equality tests, comparison of functions will cause a run-time exception; comparison of cyclic structures may not terminate.
虽然列表具有自然的递归定义和动态可变的长度,但它们的不变性和线性时间访问成本(对于任意元素)使它们对于许多应用程序来说并不理想。因此,OCaml 提供了一种更传统的数组类型。数组的长度在制定时(即在运行时遇到其声明时)是固定的,但其元素可以在常量时间内访问,并且它们的值可以通过命令式代码更改。
While lists have a natural recursive definition and dynamically variable length, their immutability and linear-time access cost (for an arbitrary element) make them less than ideal for many applications. OCaml therefore provides a more conventional array type. The length of an array is fixed at elaboration time (i.e., when its declaration is encountered at run time), but its elements can be accessed in constant time, and their values can be changed by imperative code.
模式匹配(尤其是针对字符串的模式匹配)出现在许多编程语言中。示例包括 Snobol、Icon、Perl 以及采用了 Perl 功能的几种其他脚本语言(在14.4.2 节中讨论)。ML 的独特之处在于将模式匹配扩展到所有构造值(包括元组、列表、记录和变体),并将其与静态类型和类型推断集成在一起。
Pattern matching, particularly for strings, appears in many programming languages. Examples include Snobol, Icon, Perl, and the several other scripting languages that have adopted Perl's facilities (discussed in Section 14.4.2). ML is distinctive in extending pattern matching to the full range of constructed values—including tuples, lists, records, and variants—and integrating it with static typing and type inference.
默认情况下,OCaml 编译器将对任何选项不详尽的模式匹配发出编译时警告— 即其结构未包含候选表达式类型所固有的所有可能性,因此其执行可能会导致运行时异常。如果多路匹配的后一分支中的模式完全被前一分支中的模式覆盖(意味着后者永远不会被选择),编译器也会发出警告。
By default, the OCaml compiler will issue a compile-time warning for any pattern match whose options are not exhaustive—i.e., whose structure does not include all the possibilities inherent in the type of the candidate expression, and whose execution might therefore lead to a run-time exception. The compiler will also issue a warning if the pattern in a later arm of a multi-way match is completely covered by one in an earlier arm (implying that the latter will never be chosen).
I/O 是一种常见的副作用形式。OCaml 提供标准库例程来读取和打印各种内置类型。它还支持 C 的printf风格的格式化输出。
I/O is a common form of side effect. OCaml provides standard library routines to read and print a variety of built-in types. It also supports formatted output in the style of C's printf.
在6.6.2 节中,我们观察到许多表达式的子组件可以按多种顺序求值。具体来说,可以选择在将函数参数传递给函数之前对其进行求值,或者不求值就传递它们。前一种选择称为应用顺序求值;后者称为正常顺序求值。与大多数命令式语言一样,Scheme 和 OCaml 在大多数情况下使用应用顺序。在命令式语言的宏和按名称调用参数中出现的正常顺序在特殊情况下可用。
In Section 6.6.2 we observed that the subcomponents of many expressions can be evaluated in more than one order. In particular, one can choose to evaluate function arguments before passing them to a function, or to pass them unevaluated. The former option is called applicative-order evaluation; the latter is called normal-order evaluation. Like most imperative languages, Scheme and OCaml use applicative order in most cases. Normal order, which arises in the macros and call-by-name parameters of imperative languages, is available in special cases.
在 Scheme 概述中,我们多次区分了特殊形式和函数。函数的参数始终通过共享传递(第 9.3.1 节),并在传递之前进行求值(即按应用顺序)。特殊形式的参数不经求值传递 — 换句话说,按名称传递。每个特殊形式都可以自由选择何时(以及是否)对其参数求值。例如, Cond以一系列未求值的对作为参数。它在内部一次一个地求值它们的car,当找到一个求值为#t 的car 时停止。
In our overview of Scheme we differentiated on several occasions between special forms and functions. Arguments to functions are always passed by sharing (Section 9.3.1), and are evaluated before they are passed (i.e., in applicative order). Arguments to special forms are passed unevaluated—in other words, by name. Each special form is free to choose internally when (and if) to evaluate its parameters. Cond, for example, takes a sequence of unevaluated pairs as arguments. It evaluates their cars internally, one at a time, stopping when it finds one that evaluates to #t.
在 Scheme 中,特殊形式和函数统称为表达式类型。一些表达式类型是原始的,因为它们必须内置于语言实现中。其他表达式类型是派生的;它们可以用原始表达式类型来定义。在基于eval/apply的解释器中,原始特殊形式内置于eval中;原始函数可由apply识别。我们已经了解了如何使用特殊形式lambda创建派生函数,这些函数可以用let绑定到名称。Scheme 提供了一种类似的特殊形式syntax-rules,可用于创建派生特殊形式。然后可以使用define-syntax和let-syntax将它们绑定到名称。派生特殊形式在 Scheme 中称为宏,但与大多数其他宏不同,它们是卫生的— 词法作用域,集成到语言的语义中,并且不会出现第 3.7 节中描述的错误分组和变量捕获问题。与 C++ 模板(第 C-7.3.2 节)一样,Scheme 宏是图灵完备的。它们的行为类似于通过名称传递参数的函数(第 C-9.3.2 节),而不是通过共享传递参数。但它们是通过解释器的解析器和语义分析器中的逻辑扩展来实现的,而不是通过使用 thunk 的延迟求值来实现的。
Together, special forms and functions are known as expression types in Scheme. Some expression types are primitive, in the sense that they must be built into the language implementation. Others are derived; they can be defined in terms of primitive expression types. In an eval/apply-based interpreter, primitive special forms are built into eval; primitive functions are recognized by apply. We have seen how the special form lambda can be used to create derived functions, which can be bound to names with let. Scheme provides an analogous special form, syntax-rules, that can be used to create derived special forms. These can then be bound to names with define-syntax and let-syntax. Derived special forms are known as macros in Scheme, but unlike most other macros, they are hygienic—lexically scoped, integrated into the language's semantics, and immune from the problems of mistaken grouping and variable capture described in Section 3.7. Like C++ templates (Section C-7.3.2), Scheme macros are Turing complete. They behave like functions whose arguments are passed by name (Section C-9.3.2) instead of by sharing. They are implemented, however, via logical expansion in the interpreter's parser and semantic analyzer, rather than by delayed evaluation with thunks.
求值顺序不仅会影响执行速度,还会影响程序的正确性。在应用顺序求值下遇到动态语义错误或“不需要的”子表达式中的无限回归的程序可能会在正常顺序求值下成功终止。如果一个(无副作用的)函数在其任何参数未定义时未定义(无法终止或遇到错误),则该函数被称为严格函数。这样的函数可以安全地求值其所有参数,因此其结果将不依赖于求值顺序。如果函数不施加此要求,即有时即使其参数之一未定义,该函数也定义,则该函数被称为非严格函数。如果一种语言的定义方式使得函数始终是严格的,则该语言被称为严格语言。如果一种语言允许定义非严格函数,则该语言被称为非严格语言。如果一种语言始终按应用顺序求值表达式,则每个函数都保证是严格的,因为只要参数未定义,其求值就会失败,传递给该函数的函数也会失败。相反,非严格语言不能使用应用顺序;它必须使用正常顺序以避免评估不需要的参数。标准 ML、OCaml 和(宏除外)Scheme 是严格的。Miranda 和 Haskell 是非严格的。
Evaluation order can have an effect not only on execution speed but also on program correctness. A program that encounters a dynamic semantic error or an infinite regression in an “unneeded” subexpression under applicative-order evaluation may terminate successfully under normal-order evaluation. A (side-effect-free) function is said to be strict if it is undefined (fails to terminate, or encounters an error) when any of its arguments is undefined. Such a function can safely evaluate all its arguments, so its result will not depend on evaluation order. A function is said to be nonstrict if it does not impose this requirement—that is, if it is sometimes defined even when one of its arguments is not. A language is said to be strict if it is defined in such a way that functions are always strict. A language is said to be nonstrict if it permits the definition of nonstrict functions. If a language always evaluates expressions in applicative order, then every function is guaranteed to be strict, because whenever an argument is undefined, its evaluation will fail and so will the function to which it is being passed. Contrapositively, a nonstrict language cannot use applicative order; it must use normal order to avoid evaluating unneeded arguments. Standard ML, OCaml, and (with the exception of macros) Scheme are strict. Miranda and Haskell are nonstrict.
自动实现的惰性求值为我们提供了正常顺序求值的优势(不求值不需要的子表达式),同时在需要所有内容的表达式中,其运行速度是应用顺序求值速度的常数倍。诀窍是在内部为每个参数标记一个“备忘录”,以指示其值(如果已知)。任何试图求值参数的尝试都会将备忘录中的值设置为副作用,或者如果已经设置了值,则返回该值(无需重新计算)。
Lazy evaluation, implemented automatically, gives us the advantage of normal-order evaluation (not evaluating unneeded subexpressions) while running within a constant factor of the speed of applicative-order evaluation for expressions in which everything is needed. The trick is to tag every argument internally with a “memo” that indicates its value, if known. Any attempt to evaluate the argument sets the value in the memo as a side effect, or returns the value (without recalculating it) if it is already set.
惰性求值对于“无限”数据结构特别有用,如第 6.6.2 节所述。它在只需要检查可能很长的列表的前缀的程序中也很有用(参见练习 11.10)。惰性求值用于 Miranda 和 Haskell 中的所有参数。在 Scheme 中,它通过显式使用延迟和强制6以及在 OCaml 中通过标准Lazy库的类似机制。在 Scheme 中(在某些情况下)也可以通过使用宏来隐式实现。正常顺序求值可以被认为是使用按名称调用参数的函数求值,而惰性求值有时被称为采用“按需调用”。除了 Miranda 和 Haskell 之外,在统计学家广泛使用的 R 脚本语言中也可以找到按需调用。
Lazy evaluation is particularly useful for “infinite” data structures, as described in Section 6.6.2. It can also be useful in programs that need to examine only a prefix of a potentially long list (see Exercise 11.10). Lazy evaluation is used for all arguments in Miranda and Haskell. It is available in Scheme through explicit use of delay and force,6 and in OCaml through the similar mechanisms of the standard Lazy library. It can also be achieved implicitly in Scheme (in certain contexts) through the use of macros. Where normal-order evaluation can be thought of as function evaluation using call-by-name parameters, lazy evaluation is sometimes said to employ “call by need.” In addition to Miranda and Haskell, call by need can be found in the R scripting language, widely used by statisticians.
惰性求值的主要问题是其在存在副作用时的行为。如果参数包含对可通过赋值修改的变量的引用,则参数的值将取决于它是在赋值之前还是之后求值。同样,如果参数包含赋值,程序中其他地方的值可能取决于求值发生的时间。这些问题不会出现在 Miranda 或 Haskell 中,因为它们是纯函数式的:没有副作用。Scheme 和 OCaml 将问题留给程序员,但要求每次使用delay表达式时都将其括在force中,这样可以相对容易地识别出存在副作用的地方。
The principal problem with lazy evaluation is its behavior in the presence of side effects. If an argument contains a reference to a variable that may be modified by an assignment, then the value of the argument will depend on whether it is evaluated before or after the assignment. Likewise, if the argument contains an assignment, values elsewhere in the program may depend on when evaluation occurs. These problems do not arise in Miranda or Haskell because they are purely functional: there are no side effects. Scheme and OCaml leave the problem up to the programmer, but require that every use of a delay-ed expression be enclosed in force, making it relatively easy to identify the places where side effects are an issue.
在传统 I/O 中可以发现一个主要的副作用来源:输入例程通常每次调用时都会返回不同的值;如果要认为程序正确,则对输出例程的多次调用必须按正确的顺序进行,尽管它们从不返回任何值。
A major source of side effects can be found in traditional I/O: an input routine will generally return a different value every time it is called, and multiple calls to an output routine, though they never return a value, must occur in the proper order if the program is to be considered correct.
流是 Haskell 早期版本中 I/O 系统的基础。不幸的是,虽然流成功地封装了终端交互的命令式本质,但它对于图形或随机访问文件来说效果并不好。它们还使得适应不同类型的 I/O 变得困难(因为 Haskell 中列表的所有元素都必须是单一类型)。Haskell 的较新版本使用了一个更通用的概念,称为monads。monad 源自数学的一个分支,称为范畴论,但人们不需要理解该理论就可以体会到它们在实践中的用处。在 Haskell 中,monad 本质上是高阶函数的巧妙使用,再加上一些语法糖,允许程序员将一系列必须按顺序发生的动作(函数调用)链接在一起。这个想法的强大之处在于能够将任意复杂度的隐藏结构化值从一个动作传递到下一个动作。在 monad 的许多应用中,这个额外的隐藏值扮演着可变状态的角色:携带到连续动作的值之间的差异充当了副作用。
Streams formed the basis of the I/O system in early versions of Haskell. Unfortunately, while they successfully encapsulate the imperative nature of interaction at a terminal, streams don't work very well for graphics or random access to files. They also make it difficult to accommodate I/O of different kinds (since all elements of a list in Haskell must be of a single type). More recent versions of Haskell employ a more general concept known as monads. Monads are drawn from a branch of mathematics known as category theory, but one doesn't need to understand the theory to appreciate their usefulness in practice. In Haskell, monads are essentially a clever use of higher-order functions, coupled with a bit of syntactic sugar, that allow the programmer to chain together a sequence of actions (function calls) that have to happen in order. The power of the idea comes from the ability to carry a hidden, structured value of arbitrary complexity from one action to the next. In many applications of monads, this extra hidden value plays the role of mutable state: differences between the values carried to successive actions act as side effects.
当然,也可以通过在函数主体内嵌套显式fun符号来定义fold_left 。但是,使用并列参数的简写符号更加直观和方便。
It is of course possible to define fold_left by nesting occurrences of the explicit fun notation within the function's body. The shorthand notation, with juxtaposed arguments, however, is substantially more intuitive and convenient.
更深入地
IN MORE DEPTH
Lambda 演算是一种函数定义的构造符号。我们在配套网站上对此进行了更详细的讨论。任何可计算函数都可以写成lambda 表达式。计算相当于将参数宏替换到函数定义中,然后通过简单而机械的重写规则将其简化为最简形式。应用这些规则的顺序反映了应用顺序和正常顺序求值之间的区别,如第6.6.2 节所述。某些简单函数(例如恒等函数)的使用约定允许将选择、结构甚至算术捕获为 lambda 表达式。递归是通过不动点的概念来捕获的。
Lambda calculus is a constructive notation for function definitions. We consider it in more detail on the companion site. Any computable function can be written as a lambda expression. Computation amounts to macro substitution of arguments into the function definition, followed by reduction to simplest form via simple and mechanical rewrite rules. The order in which these rules are applied captures the distinction between applicative and normal-order evaluation, as described in Section 6.6.2. Conventions on the use of certain simple functions (e.g., the identity function) allow selection, structures, and even arithmetic to be captured as lambda expressions. Recursion is captured through the notion of fixed points.
无副作用编程是一个非常有吸引力的想法。如第 6.1.2和6.3节所述,副作用会使程序难读且难以编译。相比之下,没有副作用则使表达式在引用上透明— 与求值顺序无关。纯函数式语言的程序员和编译器可以使用方程推理,其中两个表达式在任何时间点的等价意味着它们在始终等价。反过来,方程推理对于并行执行非常有吸引力:在纯函数式语言中,函数的参数可以安全地彼此并行求值。在惰性函数式语言中,它们可以与传递它们的函数(的开头)并行求值。我们将在第13.4.5 节中进一步考虑这些可能性。
Side-effect-free programming is a very appealing idea. As discussed in Sections 6.1.2 and 6.3, side effects can make programs both hard to read and hard to compile. By contrast, the lack of side effects makes expressions referentially transparent—independent of evaluation order. Programmers and compilers of a purely functional language can employ equational reasoning, in which the equivalence of two expressions at any point in time implies their equivalence at all times. Equational reasoning in turn is highly appealing for parallel execution: In a purely functional language, the arguments to a function can safely be evaluated in parallel with each other. In a lazy functional language, they can be evaluated in parallel with (the beginning of) the function to which they are passed. We will consider these possibilities further in Section 13.4.5.
不幸的是,在常见的编程习惯用法中,典型的副作用——赋值——起着核心作用。函数式编程的批评者经常指出这些习惯用法是需要命令式语言特性的证据。I/O 就是一个例子。我们已经看到(在第 11.5 节中),可以使用流以函数式方式对文件的顺序访问进行建模。对于图形和随机文件访问,我们还看到 Haskell 的 monad 可以干净地将操作的调用与语言的主体隔离开来,并允许将方程推理的全部功能应用于值的计算和 I/O 操作发生顺序的确定。
Unfortunately, there are common programming idioms in which the canonical side effect—assignment—plays a central role. Critics of functional programming often point to these idioms as evidence of the need for imperative language features. I/O is one example. We have seen (in Section 11.5) that sequential access to files can be modeled in a functional manner using streams. For graphics and random file access we have also seen that the monads of Haskell can cleanly isolate the invocation of actions from the bulk of the language, and allow the full power of equational reasoning to be applied to both the computation of values and the determination of the order in which I/O actions should occur.
其他常见的“自然命令式”习语的例子包括
Other commonly cited examples of “naturally imperative” idioms include
复杂结构的初始化:Lisp 和 ML 系列对列表的严重依赖反映了函数可以轻松地从旧列表的组件构建新列表。其他数据结构(尤其是多维数组)则不那么容易逐步组合,特别是如果初始化元素的自然顺序不是严格的行优先或列优先。
Initialization of complex structures: The heavy reliance on lists in the Lisp and ML families reflects the ease with which functions can build new lists out of the components of old lists. Other data structures—multidimensional arrays in particular—are much less easy to put together incrementally, particularly if the natural order in which to initialize the elements is not strictly row-major or column-major.
摘要:许多程序都包含扫描大型数据结构或大量输入数据的代码,用于计算各种项目或模式的出现次数。跟踪计数的自然方法是使用字典数据结构,在该结构中,人们会反复更新与最近注意到的键相关联的计数。
Summarization: Many programs include code that scans a large data structure or a large amount of input data, counting the occurrences of various items or patterns. The natural way to keep track of the counts is with a dictionary data structure in which one repeatedly updates the count associated with the most recently noticed key.
就地变异:在具有非常大数据集的程序中,必须尽可能节省内存使用量,以最大限度地增加内存或缓存中可容纳的数据量。例如,排序程序需要就地排序,而不是将元素复制到新数组或列表中。同样,基于矩阵的科学程序需要就地更新值。
In-place mutation: In programs with very large data sets, one must economize as much as possible on memory usage, to maximize the amount of data that will fit in memory or the cache. Sorting programs, for example, need to sort in place, rather than copying elements to a new array or list. Matrix-based scientific programs, likewise, need to update values in place.
最后这三个习惯用法是所谓的平凡更新问题的例子。如果使用函数式语言时,底层实现每次必须更改其元素之一时都强制创建整个数据结构的新副本,那么结果将非常低效。在命令式程序中,通过允许就地修改现有结构可以避免此问题。
These last three idioms are examples of what has been called the trivial update problem. If the use of a functional language forces the underlying implementation to create a new copy of the entire data structure every time one of its elements must change, then the result will be very inefficient. In imperative programs, the problem is avoided by allowing an existing structure to be modified in place.
有人可能会说,虽然琐碎的更新问题给 Lisp 及其相关语言带来了麻烦,但它并不反映函数式编程本身的固有弱点。解决方案需要结合方便的符号(用于访问复杂结构的任意元素)和能够确定旧版本结构何时不再使用的实现,以便可以就地更新而不是复制。
One can argue that while the trivial update problem causes trouble in Lisp and its relatives, it does not reflect an inherent weakness of functional programming per se. What is required for a solution is a combination of convenient notation—to access arbitrary elements of a complex structure—and an implementation that is able to determine when the old version of the structure will never be used again, so it can be updated in place instead of being copied.
Sisal、pH 和单赋值 C (SAC) 将数组类型和迭代语法与纯函数语义相结合。迭代构造被定义为尾递归函数的语法糖。嵌套时,这些构造可轻松用于初始化多维数组。该语言的语义表明循环的每次迭代都会返回整个数组的新副本。但是,编译器可以轻松验证返回后从未使用过旧副本,因此可以安排就地执行所有更新。在没有命令式语法的情况下也可以执行类似的优化,但需要更复杂的分析。Cann 报告称,Livermore Sisal 编译器在标准数值基准测试中,能够消除 99% 到 100% 的所有复制操作 [ Can92 ]。Scholz 报告称,SAC 的性能可与经过精心优化的现代 Fortran 程序相媲美 [ Sch03 ]。
Sisal, pH, and Single Assignment C (SAC) combine array types and iterative syntax with purely functional semantics. The iterative constructs are defined as syntactic sugar for tail-recursive functions. When nested, these constructs can easily be used to initialize a multidimensional array. The semantics of the language say that each iteration of the loop returns a new copy of the entire array. The compiler can easily verify, however, that the old copy is never used after the return, and can therefore arrange to perform all updates in place. Similar optimizations could be performed in the absence of the imperative syntax, but require somewhat more complex analysis. Cann reports that the Livermore Sisal compiler was able to eliminate 99 to 100 percent of all copy operations in standard numeric benchmarks [Can92]. Scholz reports performance for SAC competitive with that of carefully optimized modern Fortran programs [Sch03].
近年来,函数式编程的理论和实践都取得了长足的进步。Wadler [ Wad98b ] 在 20 世纪 90 年代末指出,广泛采用函数式语言的主要障碍是社会和商业方面的,而不是技术方面的:大多数程序员都接受过命令式风格的训练;函数式编程的软件库和开发环境尚不如命令式语言成熟。过去十年的经验似乎证实了这一特点:随着更好工具的开发和实践经验的不断积累,函数式语言开始得到更广泛的应用。函数式特性也开始出现在 C#、Python 和 Ruby 等主流命令式语言中。
Significant strides in both the theory and practice of functional programming have been made in recent years. Wadler [Wad98b] argued in the late 1990s that the principal remaining obstacles to the widespread adoption of functional languages were social and commercial, not technical: most programmers have been trained in an imperative style; software libraries and development environments for functional programming are not yet as mature as those of their imperative cousins. Experience over the past decade appears to have borne out this characterization: with the development of better tools and a growing body of practical experience, functional languages have begun to see much wider use. Functional features have also begun to appear in such mainstream imperative languages as C#, Python, and Ruby.
在本章中,我们重点讨论了函数式计算模型。命令式程序主要通过迭代和副作用(即修改变量)进行计算,而函数式程序主要通过将参数代入函数进行计算。我们首先列举了函数式编程中的一系列关键问题,包括一等函数和高阶函数、多态性、控制流和求值顺序以及支持基于列表的数据。然后,我们转向了两个具体的例子——Lisp 的 Scheme 方言和 ML 的 OCaml 方言——以了解如何在编程语言中解决这些问题。我们还简要介绍了 Haskell 中的惰性求值和 monad。
In this chapter we have focused on the functional model of computing. Where an imperative program computes principally through iteration and side effects (i.e., the modification of variables), a functional program computes principally through substitution of parameters into functions. We began by enumerating a list of key issues in functional programming, including first-class and higher-order functions, polymorphism, control flow and evaluation order, and support for list-based data. We then turned to a pair of concrete examples—the Scheme dialect of Lisp and the OCaml dialect of ML—to see how these issues may be addressed in a programming language. We also considered, more briefly, the lazy evaluation and monads found in Haskell.
对于命令式编程语言,底层形式模型通常被认为是图灵机。对于函数式语言,该模型是 lambda 演算。这两种模型都是在数学界发展起来的,作为形式化有效程序概念的一种手段,用于构造性证明。除了硬件对算术精度、磁盘和内存空间等的限制外,lambda 演算的全部功能在函数式语言中都可用。虽然对 lambda 演算的完整处理很容易占用另一本书,但我们在配套网站上提供了概述。我们考虑了重写规则、求值顺序和 Church-Rosser 定理。我们注意到,使用非常简单的符号的约定提供了整数算术、选择、递归和结构化数据类型的计算能力。
For imperative programming languages, the underlying formal model is often taken to be a Turing machine. For functional languages, the model is the lambda calculus. Both models evolved in the mathematical community as a means of formalizing the notion of an effective procedure, as used in constructive proofs. Aside from hardware-imposed limits on arithmetic precision, disk and memory space, and so on, the full power of lambda calculus is available in functional languages. While a full treatment of the lambda calculus could easily consume another book, we provided an overview on the companion site. We considered rewrite rules, evaluation order, and the Church-Rosser theorem. We noted that conventions on the use of very simple notation provide the computational power of integer arithmetic, selection, recursion, and structured data types.
出于实际原因,许多函数式语言都扩展了 lambda 演算,增加了其他功能,包括赋值、I/O 和迭代。此外,Lisp 方言是同音的:程序看起来像普通的数据结构,可以即时创建、修改和执行。
For practical reasons, many functional languages extend the lambda calculus with additional features, including assignment, I/O, and iteration. Lisp dialects, moreover, are homoiconic: programs look like ordinary data structures, and can be created, modified, and executed on the fly.
列表在大多数函数式程序中占有重要地位,主要是因为它们可以轻松地逐步构建,而无需将分配和修改状态作为单独的操作。许多函数式语言也提供其他结构化数据类型。在 Sisal 和单赋值 C 中,强调迭代语法、尾递归语义和高性能编译器,使基于多维数组的函数式程序能够实现与命令式程序相当的性能。
Lists feature prominently in most functional programs, largely because they can easily be built incrementally, without the need to allocate and then modify state as separate operations. Many functional languages provide other structured data types as well. In Sisal and Single Assignment C, an emphasis on iterative syntax, tail-recursive semantics, and high-performance compilers allows multidimensional array-based functional programs to achieve performance comparable to that of imperative programs.
11.1 Scheme 的 define原语是命令式语言特性吗?为什么是或为什么不是?
11.1 Is the define primitive of Scheme an imperative language feature? Why or why not?
11.2 可以用命令式语言(如 C)的纯函数式子集编写程序,但该语言的某些局限性很快就会显现出来。需要向你最喜欢的命令式语言添加哪些功能才能使其真正成为有用的函数式语言?(提示:Scheme 有什么是 C 所缺乏的?)
11.2 It is possible to write programs in a purely functional subset of an imperative language such as C, but certain limitations of the language quickly become apparent. What features would need to be added to your favorite imperative language to make it genuinely useful as a functional language? (Hint: What does Scheme have that C lacks?)
11.3 解释短路布尔表达式和正常顺序求值之间的联系。为什么cond在 Scheme 中是一种特殊形式,而不是函数?
11.3 Explain the connection between short-circuit Boolean expressions and normal-order evaluation. Why is cond a special form in Scheme, rather than a function?
11.4用你最喜欢的命令式语言编写一个程序,该程序具有与 图 11.1中的 Scheme 程序相同的输入和输出。根据你的经验,你能对 Scheme 在符号计算方面的实用性做出一般性观察吗?
11.4 Write a program in your favorite imperative language that has the same input and output as the Scheme program of Figure 11.1. Can you make any general observations about the usefulness of Scheme for symbolic computation, based on your experience?
11.5 假设我们希望从列表中删除相邻的重复元素(例如,在排序之后)。以下 Scheme 函数可实现此目标:(define unique (lambda (L) (cond ((null? L) L) ((null? (cdr L)) L) ((eqv? (car L) (car (cdr L))) (unique (cdr L))) (else (cons (car L) (unique (cdr L)))))))编写一个类似的函数,使用 Scheme 的命令式特性“就地”修改L,而不是构建新列表。将您的函数与上面的代码在简洁性、概念清晰度和速度方面进行比较。
11.5 Suppose we wish to remove adjacent duplicate elements from a list (e.g., after sorting). The following Scheme function accomplishes this goal:
(define unique
(lambda (L)
(cond
((null? L) L)
((null? (cdr L)) L)
((eqv? (car L) (car (cdr L))) (unique (cdr L)))
(else (cons (car L) (unique (cdr L)))))))
Write a similar function that uses the imperative features of Scheme to modify L “in place,” rather than building a new list. Compare your function to the code above in terms of brevity, conceptual clarity, and speed.
11.6 写出下面的尾递归版本:
11.6 Write tail-recursive versions of the following:
(a) ;; 计算以 2 为底的整数对数;;(二进制表示的位数);; 仅适用于正整数(定义 log2 (lambda(n)(if(= n 1)1(+ 1(log2(商 n 2))))))
(a) ;; compute integer log, base 2
;; (number of bits in binary representation)
;; works only for positive integers
(define log2
(lambda (n)
(if (= n 1) 1 (+ 1 (log2 (quotient n 2))))))
(b) ;; 在列表中查找最小元素(define min (lambda (l) (cond ((null? l) '()) ((null? (cdr l)) (car l)) (#t (let ((a (car l)) (b (min (cdr l)))) (if (< ba) ba))))))
(b) ;; find minimum element in a list
(define min
(lambda (l)
(cond
((null? l) '())
((null? (cdr l)) (car l))
(#t (let ((a (car l))
(b (min (cdr l))))
(if (< b a) b a))))))
11.7 编写纯函数式 Scheme 函数
11.7 Write purely functional Scheme functions to
(a) 返回给定列表的所有旋转。例如,(rotate '(abcde))应返回((abcde) (bcdea) (cdeab) (deabc) (eabcd))(按某种顺序)。
(a) return all rotations of a given list. For example, (rotate '(a b c d e)) should return ((a b c d e) (b c d e a) (c d e a b) (d e a b c) (e a b c d)) (in some order).
(b) 返回一个列表,其中包含给定列表中满足给定谓词的所有元素。例如,(filter (lambda (x) (< x 5)) '(3 9 5 8 2 4 7))应返回(3 2 4)。
(b) return a list containing all elements of a given list that satisfy a given predicate. For example, (filter (lambda (x) (< x 5)) '(3 9 5 8 2 4 7)) should return (3 2 4).
11.8 编写一个纯函数式 Scheme 函数,返回给定列表的所有排列列表。例如,给定(abc),它应该返回((abc) (bac) (bca) (acb) (cab) (cba)) (按某种顺序)。
11.8 Write a purely functional Scheme function that returns a list of all permutations of a given list. For example, given (a b c), it should return ((a b c) (b a c) (b c a) (a c b) (c a b) (c b a)) (in some order).
11.9修改 图 11.1中的 Scheme 程序或图 11.3中的 OCaml 程序,使其模拟 NFA(非确定性有限自动机),而不是 DFA。(这些自动机之间的区别在2.2.1 节中描述。)由于面对多值时你无法正确“猜测”,转换函数,您将需要使用明确编码的回溯来搜索接受的一系列移动(如果有的话),或者跟踪机器在给定时间点可能处于的所有可能状态。
11.9 Modify the Scheme program of Figure 11.1 or the OCaml program of Figure 11.3 to simulate an NFA (nondeterministic finite automaton), rather than a DFA. (The distinction between these automata is described in Section 2.2.1.) Since you cannot “guess” correctly in the face of a multivalued transition function, you will need either to use explicitly coded backtracking to search for an accepting series of moves (if there is one), or keep track of all possible states that the machine could be in at a given point in time.
11.10 考虑判断两棵树是否具有相同边缘的问题:无论内部结构如何,具有相同顺序的相同叶子集。解决这个问题的一个显而易见的方法是编写一个函数flatten,它以一棵树作为参数并返回其叶子的有序列表。然后我们可以说(define same-fringe (lambda (T1 T2) (equal (flatten T1) (flatten T2))))在 Scheme 中编写一个简单版本的flatten 。当两棵树的前几个叶子不同时, same-fringe 的效率如何?在像 Haskell 这样对所有参数都使用惰性求值的语言中,你的答案会有什么不同?使用延迟和强制,在 Scheme 中获得 Haskell 的行为有多难?
11.10 Consider the problem of determining whether two trees have the same fringe: the same set of leaves in the same order, regardless of internal structure. An obvious way to solve this problem is to write a function flatten that takes a tree as argument and returns an ordered list of its leaves. Then we can say
(define same-fringe
(lambda (T1 T2)
(equal (flatten T1) (flatten T2))))
Write a straightforward version of flatten in Scheme. How efficient is same-fringe when the trees differ in their first few leaves? How would your answer differ in a language like Haskell, which uses lazy evaluation for all arguments? How hard is it to get Haskell's behavior in Scheme, using delay and force?
11.11 在示例 11.59中,我们展示了如何根据流的惰性求值实现交互式 I/O。遗憾的是,我们的代码无法按编写的方式工作,因为 Scheme 使用应用顺序求值。不过,我们可以通过调用delay和force使其工作。
假设我们将input定义为一个返回“istream”的函数——一个承诺,当强制执行时将产生一个对,其cdr是一个 istream: (define input (lambda () (delay (cons (read) (input)))))现在我们可以定义驱动程序以期望一个“ostream”——一个空列表或一个对,其cdr是一个 ostream: (define driver (lambda (s) (if (null? s) '() (display (car s)) (driver (force (cdr s))))))注意force的使用。说明如何编写函数squares以使其接受 istream 作为参数并返回 ostream。然后您应该能够输入(driver(squares(input)))并看到适当的行为。
11.11 In Example 11.59 we showed how to implement interactive I/O in terms of the lazy evaluation of streams. Unfortunately, our code would not work as written, because Scheme uses applicative-order evaluation. We can make it work, however, with calls to delay and force.
Suppose we define input to be a function that returns an “istream”—a promise that when forced will yield a pair, the cdr of which is an istream:
(define input (lambda () (delay (cons (read) (input)))))
Now we can define the driver to expect an “ostream”—an empty list or a pair, the cdr of which is an ostream:
(define driver
(lambda (s)
(if (null? s) '()
(display (car s))
(driver (force (cdr s))))))
Note the use of force.
Show how to write the function squares so that it takes an istream as argument and returns an ostream. You should then be able to type (driver (squares (input))) and see appropriate behavior.
11.12 编写操作流的cons、car和cdr的新版本。使用它们重写上一个练习的代码,以消除对delay和force 的调用。请注意,流版本的cons将需要避免评估其第二个参数;您需要学习如何在 Scheme 中定义宏(派生的特殊形式)。
11.12 Write new versions of cons, car, and cdr that operate on streams. Using them, rewrite the code of the previous exercise to eliminate the calls to delay and force. Note that the stream version of cons will need to avoid evaluating its second argument; you will need to learn how to define macros (derived special forms) in Scheme.
11.13 用 Scheme 编写标准快速排序算法,不要使用任何命令式语言特性。小心避免琐碎的更新问题;您的代码应在预期时间内运行n log n。
使用数组重写您的代码(您可能需要查阅 Scheme 手册以获取更多信息)。比较两种排序的运行时间和空间要求。
11.13 Write the standard quicksort algorithm in Scheme, without using any imperative language features. Be careful to avoid the trivial update problem; your code should run in expected time n log n.
Rewrite your code using arrays (you will probably need to consult a Scheme manual for further information). Compare the running time and space requirements of your two sorts.
11.14 编写插入和查找例程来操作 Scheme 中的二叉搜索树(如果需要更多信息,请查阅算法文本)。解释为什么简单的更新问题不会影响插入的渐近性能。
11.14 Write insert and find routines that manipulate binary search trees in Scheme (consult an algorithms text if you need more information). Explain why the trivial update problem does not impact the asymptotic performance of insert.
11.15 用纯函数式 Scheme 编写一个 LL(1) 解析器生成器。如果你参考图 2.24,请记住你需要使用尾递归代替迭代。假设输入 CFG 由一个列表列表组成,每个列表对应一个语法中的非终结符。每个子列表的第一个元素应该是非终结符;其余元素应该是产生式的右侧,其中非终结符是左侧。你可以假设起始符号的子列表将是列表中的第一个。如果我们使用带引号的字符串来表示语法符号,则图 2.16中的计算器语法将如下所示:'((“program” (“stmt_list” “$$”)) (“stmt_list” (“stmt” “stmt_list”) ()) (“stmt” (“id” “:=“ “expr”) (“read” “id”) (“write” “expr”)) (“expr” (“term” “term_tail”)) (“term” (“factor” “factor_tail”)) (“term_tail” (“add_op” “term” “term_tail”) ()) (“factor_tail” (“mult_op” “factor” “FT”) ()) (“add_op” (“+”) (“−”)) (“mult_op” (“*”) (“/”)) (“factor” (“id”) (“number”) (“(“ “expr” “)”)))您的输出应为解析表具有相同的格式,不同之处在于每个右侧都被一对(2 元素列表)替换,其第一个元素是相应生产的预测集,其第二个元素是右侧。对于计算器语法,表格如下所示:((“program” ((“$$” “id” “read” “write”) (“stmt_list” “$$”))) (“stmt_list” ((“id” “read” “write”) (“stmt” “stmt_list”)) ((“$$”) ())) (“stmt” ((“id”) (“id” “:=“ “expr”)) ((“read”) (“read” “id”)) ((“write”) (“write” “expr”))) (“expr” ((“(“ “id” “number”) (“term” “term_tail”))) (“term” ((“(“ “id” “number”) (“factor” “factor_tail”))) (“term_tail” ((“+” “−”) (“add_op” “term” “term_tail”)) ((“$$” “)” “id” “read” “write”) ())) (“factor_tail” ((“*” “/”) (“mult_op” “factor” “factor_tail”)) ((“$$” “)” “+” “−” “id” “read” “write”) ())) (“add_op” ((“+”) (“+”)) ((“−”) (“−”))) (“mult_op” ((“*”) (“*”)) ((“/”) (“/”))) (“factor” ((“id”) (“id” )) ((“number”) (“number”)) ((“(“) (“ “expr” “)”))))
(提示:您可能想要定义一个right_context函数,该函数以非终结符B作为参数并返回所有对 ( A , β )的列表,其中A是非终结符,β是符号列表,这样对于某些可能不同的符号列表α,A → α B β。此函数对于计算 FOLLOW 集很有用。您可能还想构建一个尾递归函数,重新计算 FIRST 和 FOLLOW 集直到它们收敛。如果您不将ε包含在任何一个集合中,而是为每个非终结符保留一个单独的估计值,以确定它是否可能生成ε ,您会发现这样做更容易。)
11.15 Write an LL(1) parser generator in purely functional Scheme. If you consult Figure 2.24, remember that you will need to use tail recursion in place of iteration. Assume that the input CFG consists of a list of lists, one per nonterminal in the grammar. The first element of each sublist should be the nonterminal; the remaining elements should be the right-hand sides of the productions for which that nonterminal is the left-hand side. You may assume that the sublist for the start symbol will be the first one in the list. If we use quoted strings to represent grammar symbols, the calculator grammar of Figure 2.16 would look like this:
'((“program” (“stmt_list” “$$”))
(“stmt_list” (“stmt” “stmt_list”) ())
(“stmt” (“id” “:=“ “expr”) (“read” “id”) (“write” “expr”))
(“expr” (“term” “term_tail”))
(“term” (“factor” “factor_tail”))
(“term_tail” (“add_op” “term” “term_tail”) ())
(“factor_tail” (“mult_op” “factor” “FT”) ())
(“add_op” (“+”) (“−”))
(“mult_op” (“*”) (“/”))
(“factor” (“id”) (“number”) (“(“ “expr” “)”)))
Your output should be a parse table that has this same format, except that every right-hand side is replaced by a pair (a 2-element list) whose first element is the predict set for the corresponding production, and whose second element is the right-hand side. For the calculator grammar, the table looks like this:
((“program” ((“$$” “id” “read” “write”) (“stmt_list” “$$”)))
(“stmt_list”
((“id” “read” “write”) (“stmt” “stmt_list”))
((“$$”) ()))
(“stmt”
((“id”) (“id” “:=“ “expr”))
((“read”) (“read” “id”))
((“write”) (“write” “expr”)))
(“expr” ((“(“ “id” “number”) (“term” “term_tail”)))
(“term” ((“(“ “id” “number”) (“factor” “factor_tail”)))
(“term_tail”
((“+” “−”) (“add_op” “term” “term_tail”))
((“$$” “)” “id” “read” “write”) ()))
(“factor_tail”
((“*” “/”) (“mult_op” “factor” “factor_tail”))
((“$$” “)” “+” “−” “id” “read” “write”) ()))
(“add_op” ((“+”) (“+”)) ((“−”) (“−”)))
(“mult_op” ((“*”) (“*”)) ((“/”) (“/”)))
(“factor”
((“id”) (“id”))
((“number”) (“number”))
((“(“) (“(“ “expr” “)”))))
(Hint: You may want to define a right_context function that takes a nonterminal B as argument and returns a list of all pairs (A, β), where A is a nonterminal and β is a list of symbols, such that for some potentially different list of symbols α, A → α B β. This function is useful for computing FOLLOW sets. You may also want to build a tail-recursive function that recomputes FIRST and FOLLOW sets until they converge. You will find it easier if you do not include ε in either set, but rather keep a separate estimate, for each nonterminal, of whether it may generate ε.)
11.16 编写一个相等运算符(称为=/),使它能够正确地对示例 11.38中的yearday类型起作用。(您可能需要查找控制闰年发生的规则。)
11.16 Write an equality operator (call it =/) that works correctly on the yearday type of Example 11.38. (You may need to look up the rules that govern the occurrence of leap years.)
11.17 为侧栏 11.3 中介绍的摄氏和华氏温度类型创建加法和减法函数。为了允许混合使用这两种刻度,您还应该定义转换函数ct_of_ft : fahrenheit_temp -> celsius_temp和ft_of_ct : celsius_temp -> fahrenheit_temp。您的转换应该四舍五入到最接近的度数(半度向上舍入)。
11.17 Create addition and subtraction functions for the celsius and fahrenheit temperature types introduced in Sidebar 11.3. To allow the two scales to be mixed, you should also define conversion functions ct_of_ft : fahrenheit_temp -> celsius_temp and ft_of_ct : celsius_temp -> fahrenheit_temp. Your conversions should round to the nearest degree (half degrees round up).
11.18 我们可以在函数中使用封装来延迟 OCaml 中的求值:
type 'a delayed_list = Pair of 'a * 'a delayed_list | Promise of (unit -> 'a * 'a delayed_list);; let head = function | Pair (h, r) -> h | Promise (f) -> let (a, b) = f() in a;; let rest = function | Pair (h, r) -> r | Promise (f) -> let (a, b) = f() in b;;现在给定let rec next_int n = (n, Promise (fun() -> next_int (n + 1)));; let naturals = Promise (fun() -> next_int (1));;我们有
11.18 We can use encapsulation within functions to delay evaluation in OCaml:
type 'a delayed_list =
Pair of 'a * 'a delayed_list
| Promise of (unit -> 'a * 'a delayed_list);;
let head = function
| Pair (h, r) -> h
| Promise (f) -> let (a, b) = f() in a;;
let rest = function
| Pair (h, r) -> r
| Promise (f) -> let (a, b) = f() in b;;
Now given
let rec next_int n = (n, Promise (fun() -> next_int (n + 1)));;
let naturals = Promise (fun() -> next_int (1));;
we have
延迟列表naturals的长度实际上是无限的。它只会计算出实际需要的值。但是,如果一个值需要多次,则每次都会重新计算。说明如何使用指针和赋值(示例 8.42 )来记忆delayed_list的值,以便元素只计算一次。
The delayed list naturals is effectively of unlimited length. It will be computed out only as far as actually needed. If a value is needed more than once, however, it will be recomputed every time. Show how to use pointers and assignment (Example 8.42) to memoize the values of a delayed_list, so that elements are computed only once.
11.19 编写示例11.67的OCaml版本。或者(或另外),用OCaml解决练习11.5、11.7、11.8、11.10、11.13、11.14或11.15。
11.19 Write an OCaml version of Example 11.67. Alternatively (or in addition), solve Exercises 11.5, 11.7, 11.8, 11.10, 11.13, 11.14, or 11.15 in OCaml.
11.20–11.23 更深入。
11.20–11.23 In More Depth.
11.24 阅读 Lisp 的原始自我定义 [ MAE + 65 ]。将其与 Scheme 的类似定义 [ AS96,第 4 章] 进行比较。有什么不同?什么保持不变?在每个定义中, apply和eval中内置了什么?你觉得整个想法怎么样?元循环解释器真的定义了什么吗?还是说它是“循环推理”?
11.24 Read the original self-definition of Lisp [MAE+65]. Compare it to a similar definition of Scheme [AS96, Chap. 4]. What is different? What has stayed the same? What is built into apply and eval in each definition? What do you think of the whole idea? Does a metacircular interpreter really define anything, or is it “circular reasoning”?
11.25 阅读 John Backus 的图灵奖演讲 [ Bac78 ],他在演讲中主张函数式编程。他的 FP 符号与 Lisp 和 ML 语言家族相比如何?
11.25 Read the Turing Award lecture of John Backus [Bac78], in which he argues for functional programming. How does his FP notation compare to the Lisp and ML language families?
11.26 进一步了解 Haskell 中的 monad。特别注意列表的定义。解释列表 monad 与列表推导(示例 8.58)、迭代器、延续(第 6.2.2 节)和回溯搜索的关系。
11.26 Learn more about monads in Haskell. Pay particular attention to the definition of lists. Explain the relationship of the list monad to list comprehensions (Example 8.58), iterators, continuations (Section 6.2.2), and backtracking search.
11.27 提前阅读并了解事务内存(第 13.4.4 节)。然后阅读 STM Haskell [ HMPH05 ]。解释 monad 如何促进对线程间共享位置的更新序列化。
11.27 Read ahead and learn about transactional memory (Section 13.4.4). Then read up on STM Haskell [HMPH05]. Explain how monads facilitate the serialization of updates to locations shared between threads.
11.28 我们已经看到 Lisp 和 ML 包含赋值和迭代等命令式特性。这些特性有多重要?Haskell 等语言坚持纯函数式编程风格,会放弃什么(反过来说,它们会得到什么)?同样,您如何看待最近几种命令式语言(尤其是 Python 和 C#——参见边栏 11.6)尝试使用函数构造函数和无限扩展来促进函数式编程?
11.28 We have seen that Lisp and ML include such imperative features as assignment and iteration. How important are these? What do languages like Haskell give up (conversely, what do they gain) by insisting on a purely functional programming style? In a similar vein, what do you think of attempts in several recent imperative languages (notably Python and C#—see Sidebar 11.6) to facilitate functional programming with function constructors and unlimited extent?
11.29 研究函数式程序的编译。会出现哪些特殊问题?用什么技术来解决这些问题?你可以从 Appel [ App97 ]、Wilhelm 和 Maurer [ WM95 ] 以及 Grune 等人的编译器文本 [ GBJ + 12 ] 开始搜索。
11.29 Investigate the compilation of functional programs. What special issues arise? What techniques are used to address them? Starting places for your search might include the compiler texts of Appel [App97], Wilhelm and Maurer [WM95], and Grune et al. [GBJ+12].
11.30–11.32 更深入地了解。
11.30–11.32 In More Depth.
Lisp 是最初的函数式编程语言,起源于 McCarthy 及其同事在 20 世纪 50 年代末的工作。Erlang、Haskell、Lisp、Miranda、ML、OCaml、Scheme、Single Assignment C 和 Sisal 的参考书目可在附录 A中找到。历史上重要的 Lisp 方言包括 Lisp 1.5 [ MAE + 65 ]、MacLisp [ Moo78 ](与 Apple Macintosh 无关)和 Interlisp [ TM81 ]。
Lisp, the original functional programming language, dates from the work of McCarthy and his associates in the late 1950s. Bibliographic references for Erlang, Haskell, Lisp, Miranda, ML, OCaml, Scheme, Single Assignment C, and Sisal can be found in Appendix A. Historically important dialects of Lisp include Lisp 1.5 [MAE+65], MacLisp [Moo78] (no relation to the Apple Macintosh), and Interlisp [TM81].
Abelson 和 Sussman 编写的书籍 [ AS96 ] 长期以来一直用于 MIT 和其他地方的入门编程课程,它是基本编程概念(尤其是函数式编程)的经典指南。在 Hudak 的论文 [ Hud89 ] 中可以找到更多历史参考,该论文从 Haskell 的角度概述了该领域。
The book by Abelson and Sussman [AS96], long used for introductory programming classes at MIT and elsewhere, is a classic guide to fundamental programming concepts, and to functional programming in particular. Additional historical references can be found in the paper by Hudak [Hud89], which surveys the field from the point of view of Haskell.
1941 年,Church 引入了 lambda 演算 [ Chu41 ]。Curry 和 Feys 的著作 [ CF58 ] 是经典参考书。Barendregt 的书 [ Bar84 ] 是标准的现代参考书。Michaelson [ Mic89 ] 对该形式主义进行了通俗易懂的介绍,并清晰解释了其与 Lisp 和 ML 的关系。Stansifer [ Sta95,第 7.6 节] 对定点组合子Y进行了很好的非正式讨论和正确性证明(参见练习 C-11.21)。
The lambda calculus was introduced by Church in 1941 [Chu41]. A classic reference is the text of Curry and Feys [CF58]. Barendregt's book [Bar84] is a standard modern reference. Michaelson [Mic89] provides an accessible introduction to the formalism, together with a clear explanation of its relationship to Lisp and ML. Stansifer [Sta95, Sec. 7.6] provides a good informal discussion and correctness proof for the fixed-point combinator Y (see Exercise C-11.21).
Fortran 的原始开发者之一 John Backus 在 1977 年的图灵奖演讲 [ Bac78 ]中强烈主张转向函数式编程。他的函数式编程符号称为 FP。Peyton Jones [ Pey87、Pey92 ]、Wilhelm 和 Maurer [ WM95,第 3 章]、Appel [ App97,第 15 章] 和 Grune 等人 [ GBJ + 12,第 7 章] 讨论了函数式语言的实现。Peyton Jones 关于“awkward squad”的论文 [ Pey01 ] 被广泛认为是 Haskell 中对 monad 的权威介绍。
John Backus, one of the original developers of Fortran, argued forcefully for a move to functional programming in his 1977 Turing Award lecture [Bac78]. His functional programming notation is known as FP. Peyton Jones [Pey87, Pey92], Wilhelm and Maurer [WM95, Chap. 3], Appel [App97, Chap. 15], and Grune et al. [GBJ+12, Chap. 7] discuss the implementation of functional languages. Peyton Jones's paper on the “awkward squad” [Pey01] is widely considered the definitive introduction to monads in Haskell.
尽管 Lisp 诞生于 20 世纪 60 年代初,但直到最近几年,函数式语言才开始在大型商业系统中得到广泛使用。Wadler [ Wad98a、Wad98b ] 描述了 20 世纪 90 年代末形势开始转变时的情况。许多后续项目的描述都可以在自 2004 年以来每年举办的“函数式编程商业用户”研讨会 (cufp.galois.com) 的论文集中找到。《函数式编程杂志》也出版了一类关于商业用途的特别文章。Armstrong 报告 [ Arm07 ],爱立信 AXD301 是一个由两百多万行 Erlang 代码组成的电话交换系统,它实现了令人惊叹的“九个九”级可靠性 — — 相当于每年停机时间少于 32 毫秒。
While Lisp dates from the early 1960s, it is only in recent years that functional languages have seen widespread use in large commercial systems. Wadler [Wad98a, Wad98b] describes the situation as of the late 1990s, when the tide began to turn. Descriptions of many subsequent projects can be found in the proceedings of the Commercial Users of Functional Programming workshop (cufp.galois.com), held annually since 2004. The Journal of Functional Programming also publishes a special category of articles on commercial use. Armstrong reports [Arm07] that the Ericsson AXD301, a telephone switching system comprising more than two million lines of Erlang code, has achieved an astonishing “nine nines” level of reliability—the equivalent of less than 32 ms of downtime per year.
详细讨论了函数式语言之后,我们现在来讨论逻辑语言。编程语言设计中的命令式概念和函数式概念的重叠使得我们在文中多次讨论后者。我们较少有机会评论逻辑编程语言的特点。当然,逻辑在数字电路设计中被广泛使用,大多数编程语言都提供逻辑(布尔)类型和运算符。逻辑在语言语义的形式化研究中也被广泛使用,特别是在公理语义中。1在 20 世纪70年代,随着法国艾克斯-马赛大学的 Alain Colmeraurer 和 Philippe Roussel 以及苏格兰爱丁堡大学的 Robert Kowalski 及其同事的工作,研究人员也开始将逻辑推理过程用作一种通用的计算模型。
Having considered functional languages in some detail, we now turn to logic languages. The overlap between imperative and functional concepts in programming language design has led us to discuss the latter at numerous points throughout the text. We have had less occasion to remark on features of logic programming languages. Logic of course is used heavily in the design of digital circuits, and most programming languages provide a logical (Boolean) type and operators. Logic is also heavily used in the formal study of language semantics, specifically in axiomatic semantics.1 In the 1970s, with the work of Alain Colmeraurer and Philippe Roussel of the University of Aix–Marseille in France and Robert Kowalski and associates at the University of Edinburgh in Scotland, researchers also began to employ the process of logical deduction as a general-purpose model of computing.
我们在第 12.1 节中介绍了逻辑编程的基本概念。然后,我们在第 12.2 节中概述了最广泛使用的逻辑语言 Prolog 。我们依次考虑了解析和统一的概念、对列表和算术的支持以及基于搜索的执行模型。在基于井字游戏提供了一个扩展示例之后,我们将转向更高级的主题,即命令式控制流和数据库操作。
We introduce the basic concepts of logic programming in Section 12.1. We then survey the most widely used logic language, Prolog, in Section 12.2. We consider, in turn, the concepts of resolution and unification, support for lists and arithmetic, and the search-based execution model. After presenting an extended example based on the game of tic-tac-toe, we turn to the more advanced topics of imperative control flow and database manipulation.
函数式编程基于 lambda 演算的形式化,而 Prolog 和其他逻辑语言则基于一阶谓词演算。配套站点上的C-12.3 节简要介绍了这种形式化。函数式语言可以充分利用 lambda 演算的功能(至少在内存和其他资源的限制范围内),而逻辑语言则不能充分利用谓词演算的功能。我们将在第 12.4 节中对相关限制进行一般性逻辑编程评估。
Much as functional programming is based on the formalism of lambda calculus, Prolog and other logic languages are based on first-order predicate calculus. A brief introduction to this formalism appears in Section C-12.3 on the companion site. Where functional languages capture the full capabilities of the lambda calculus, however (within the limits, at least, of memory and other resources), logic languages do not capture the full power of predicate calculus. We consider the relevant limitations as part of a general evaluation of logic programming in Section 12.4.
逻辑编程系统允许程序员陈述一组公理,从中可以证明定理。逻辑程序的用户陈述一个定理或目标,语言实现尝试找到一组公理和推理步骤(包括变量值的选择),它们一起暗示了目标。在现有的几种逻辑语言中,Prolog 是迄今为止使用最广泛的。
Logic programming systems allow the programmer to state a collection of axioms from which theorems can be proven. The user of a logic program states a theorem, or goal, and the language implementation attempts to find a collection of axioms and inference steps (including choices of values for variables) that together imply the goal. Of the several existing logic languages, Prolog is by far the most widely used.
就像命令式或函数式语言解释器在定义了各种函数和常量的引用环境上下文中评估表达式一样,Prolog 解释器在假定为真的子句(Horn 子句)数据库上下文中运行。3每个子句由项组成,这些项可以是常量、变量或结构。常量可以是原子或数字。结构可以被视为逻辑谓词或数据结构。
Much as an imperative or functional language interpreter evaluates expressions in the context of a referencing environment in which various functions and constants have been defined, a Prolog interpreter runs in the context of a database of clauses (Horn clauses) that are assumed to be true.3 Each clause is composed of terms, which may be constants, variables, or structures. A constant is either an atom or a number. A structure can be thought of as either a logical predicate or a data structure.
也可以编写左侧为空的子句。这样的子句称为查询或目标。查询不会出现在 Prolog 程序中。相反,人们会构建一个事实和规则数据库,然后通过向 Prolog 解释器(或编译后的 Prolog 程序)提供要回答的查询(即要证明的目标)来启动执行。
It is also possible to write a clause with an empty left-hand side. Such a clause is called a query, or a goal. Queries do not appear in Prolog programs. Rather, one builds a database of facts and rules and then initiates execution by giving the Prolog interpreter (or the compiled Prolog program) a query to be answered (i.e., a goal to be proven).
请注意,最后一条规则右侧有一个变量 ( Z ),但该变量未出现在头部。此类变量是存在量化的:对于所有X和Y,如果存在一个他们都属于的类Z ,则X和Y是同班同学。
Note that the last rule has a variable (Z) on the right-hand side that does not appear in the head. Such variables are existentially quantified: for all X and Y, X and Y are classmates if there exists a class Z that they both take.
用于将X与jane_doe关联、将Z与cs254关联的模式匹配过程称为“统一”。通过统一而赋值的变量称为“实例化”。
The pattern-matching process used to associate X with jane_doe and Z with cs254 is known as unification. Variables that are given values as a result of unification are said to be instantiated.
Prolog 的统一规则规定
The unification rules for Prolog state that
■ A constant unifies only with itself.
■ 两个结构统一当且仅当它们具有相同的函子和相同的元数,并且相应的参数递归统一。
■ Two structures unify if and only if they have the same functor and the same arity, and the corresponding arguments unify recursively.
■ 变量与任何事物联合。如果另一事物有值,则该变量被实例化。如果另一事物是未实例化的变量,则这两个变量以这样的方式关联:如果其中一个变量稍后被赋予值,则该值将由两者共享。
■ A variable unifies with anything. If the other thing has a value, then the variable is instantiated. If the other thing is an uninstantiated variable, then the two variables are associated in such a way that if either is given a value later, that value will be shared by both.
那么 Prolog 如何回答查询(满足目标)呢?它需要的是一系列解析步骤,这些步骤将根据数据库中的子句构建目标,或者证明不存在这样的序列。在形式逻辑领域,可以想象两种主要的搜索策略:
So how does Prolog go about answering a query (satisfying a goal)? What it needs is a sequence of resolution steps that will build the goal out of clauses in the database, or a proof that no such sequence exists. In the realm of formal logic, one can imagine two principal search strategies:
■ 从现有条款开始,向前推进,尝试得出目标。此策略称为正向链接。
■ Start with existing clauses and work forward, attempting to derive the goal. This strategy is known as forward chaining.
■ 从目标开始,然后反向推进,试图将其“分解”为一组预先存在的子句。这种策略称为后向链接。
■ Start with the goal and work backward, attempting to “unresolve” it into a set of preexisting clauses. This strategy is known as backward chaining.
如果现有规则的数量非常多,但事实的数量很少,则前向链接可能比后向链接更快地找到解决方案。然而,在大多数情况下,后向链接效率更高。Prolog 被定义为使用后向链接。
If the number of existing rules is very large, but the number of facts is small, it is possible for forward chaining to discover a solution more quickly than backward chaining. In most circumstances, however, backward chaining turns out to be more efficient. Prolog is defined to use backward chaining.
Prolog 中回溯搜索的空间管理通常遵循C-9.5.3 节中描述的迭代器的单栈实现。每次开始追寻新的子目标G时,解释器都会将一个框架推送到其堆栈上。如果G失败,则从堆栈中弹出该框架,解释器开始回溯。如果G成功,控制权将返回给“调用者”(搜索树中的父级),但G的框架仍保留在堆栈上。后面的子目标将在上方获得空间 这个休眠框架。如果后续回溯导致解释器搜索满足G的其他方法,控制将能够从上次中断的地方恢复。请注意,除非 G 的所有子目标(以及搜索树中其右侧的所有兄弟)也都失败,否则G不会失败,这意味着堆栈中G的框架上方没有任何东西。在解释器的顶层,用户输入的分号被视为最近满足的子目标失败。
Space management for backtracking search in Prolog usually follows the single-stack implementation of iterators described in Section C-9.5.3. The interpreter pushes a frame onto its stack every time it begins to pursue a new subgoal G. If G fails, the frame is popped from the stack and the interpreter begins to backtrack. If G succeeds, control returns to the “caller” (the parent in the search tree), but G's frame remains on the stack. Later subgoals will be given space above this dormant frame. If subsequent backtracking causes the interpreter to search for alternative ways of satisfying G, control will be able to resume where it last left off. Note that G will not fail unless all of its subgoals (and all of its siblings to the right in the search tree) have also failed, implying that there is nothing above G's frame in the stack. At the top level of the interpreter, a semicolon typed by the user is treated the same as failure of the most recently satisfied subgoal.
我们已经看到,Prolog 中子句和术语的排序非常重要,对效率、终止和备选方案的选择都有影响。除了简单的排序之外,Prolog 还为程序员提供了几个显式的控制流功能。这些功能中最重要的一个被称为cut。
We have seen that the ordering of clauses and of terms in Prolog is significant, with ramifications for efficiency, termination, and choice among alternatives. In addition to simple ordering, Prolog provides the programmer with several explicit control-flow features. The most important of these features is known as the cut.
这种编程习惯用法(带有测试终止符的无界生成器)称为生成和测试。与 Scheme 的迭代构造(第 11.3.4 节)一样,它通常与副作用一起使用。显然,I/O 就是其中一种副作用。另一个副作用是修改程序数据库。
This programming idiom—an unbounded generator with a test-cut terminator—is known as generate-and-test. Like the iterative constructs of Scheme (Section 11.3.4), it is generally used in conjunction with side effects. One such side effect, clearly, is I/O. Another is modification of the program database.
Prolog 提供了多种 I/O 功能。除了write和nl可以打印到当前输出文件之外,read谓词还可用于从当前输入文件中读取术语。使用get和put可以读取和写入单个字符。使用see和tell可以将输入和输出重定向到不同的文件。最后,内置谓词consult和reconsult可用于从文件中读取数据库子句,因此不必手动将它们输入到解释器中。(有些解释器需要这样做,只允许以交互方式输入查询。)
Prolog provides a variety of I/O features. In addition to write and nl, which print to the current output file, the read predicate can be used to read terms from the current input file. Individual characters are read and written with get and put. Input and output can be redirected to different files using see and tell. Finally, the built-in predicates consult and reconsult can be used to read database clauses from a file, so they don't have to be typed into the interpreter by hand. (Some interpreters require this, allowing only queries to be entered interactively.)
各种其他内置谓词也可用于“解构”子句的内容。var谓词采用单个参数;当且仅当其参数为未实例化的变量时,它才会作为目标成功。原子谓词和整数谓词当且仅当它们的参数分别为原子和整数时,才会作为目标成功。名称谓词采用两个参数。当且仅当其第一个参数是原子,而第二个参数是该原子字符的 ASCII 码组成的列表时,它才会作为目标成功。
Various other built-in predicates can also be used to “deconstruct” the contents of a clause. The var predicate takes a single argument; it succeeds as a goal if and only if its argument is an uninstantiated variable. The atom and integer predicates succeed as goals if and only if their arguments are atoms and integers, respectively. The name predicate takes two arguments. It succeeds as a goal if and only if its first argument is an atom and its second is a list composed of the ASCII codes for the characters of that atom.
更深入地
IN MORE DEPTH
在传统的逻辑符号中,有许多方法可以陈述给定的命题。逻辑编程建立在子句形式上,它为每个命题提供唯一的表达式。许多(但不是全部)子句形式都可以转换为 Horn 子句的集合,从而翻译成 Prolog。在配套网站上,我们追踪了将任意命题翻译成子句形式所需的步骤。我们还描述了这种形式可以和不能翻译成 Prolog 的情况。
In conventional logical notation there are many ways to state a given proposition. Logic programming is built on clausal form, which provides a unique expression for every proposition. Many though not all clausal forms can be cast as a collection of Horn clauses, and thus translated into Prolog. On the companion site we trace the steps required to translate an arbitrary proposition into clausal form. We also characterize the cases in which this form can and cannot be translated into Prolog.
从理论上讲,逻辑编程是一个非常引人注目的想法:它提出了一种计算模型,我们只需列出未知值的逻辑属性,然后计算机就会找出如何找到它(或告诉我们它不存在)。不幸的是,由于理论和实践原因,现实与这一愿景相差甚远。
In the abstract, logic programming is a very compelling idea: it suggests a model of computing in which we simply list the logical properties of an unknown value, and then the computer figures out how to find it (or tells us it doesn't exist). Unfortunately, reality falls quite a bit short of the vision, for both theoretical and practical reasons.
如第 12.3 节所述,霍恩子句并未涵盖一阶谓词演算的全部内容。具体而言,它们不能用于表达子句形式包含具有多个非否定项的析取的语句。我们有时可以在 Prolog 中使用\+谓词来解决这个问题,但语义并不相同(参见第 12.4.3 节)。
As noted in Section 12.3, Horn clauses do not capture all of first-order predicate calculus. In particular, they cannot be used to express statements whose clausal form includes a disjunction with more than one non-negated term. We can sometimes get around this problem in Prolog by using the \+ predicate, but the semantics are not the same (see Section 12.4.3).
虽然逻辑本质上是声明性的,但大多数逻辑语言都以确定的顺序探索可能的解决方案树。 Prolog 提供了各种谓词,包括 cut、fail和repeat,以控制执行顺序(第 12.2.6 节)。它还提供了谓词,包括assert、retract和call,以在执行期间显式操作其数据库。
While logic is inherently declarative, most logic languages explore the tree of possible resolutions in deterministic order. Prolog provides a variety of predicates, including the cut, fail, and repeat, to control that execution order (Section 12.2.6). It also provides predicates, including assert, retract, and call, to manipulate its database explicitly during execution.
正如我们在第 10 章中看到的,区分程序的规范和实现很有用。规范说明了程序要做什么;实现说明了如何做。霍恩子句为规范提供了一种极好的符号。当使用搜索规则进行扩充时(如在 Prolog 中),它们允许用相同的符号来表达实现。
As we saw in Chapter 10, it can be useful to distinguish between the specification of a program and its implementation. The specification says what the program is to do; the implementation says how it is to do it. Horn clauses provide an excellent notation for specifications. When augmented with search rules (as in Prolog) they allow implementations to be expressed in the same notation.
在本章中,我们重点介绍了计算的逻辑模型。命令式程序主要通过迭代和副作用进行计算,函数式程序主要通过将参数代入函数进行计算,而逻辑程序则通过逻辑语句的解析进行计算,由统一变量和项的能力驱动。
In this chapter we have focused on the logic model of computing. Where an imperative program computes principally through iteration and side effects, and a functional program computes principally through substitution of parameters into functions, a logic program computes through the resolution of logical statements, driven by the ability to unify variables and terms.
我们的大部分讨论都是通过对主要逻辑语言 Prolog 的考察进行的,我们用它来说明条款和术语、解析和统一、搜索/执行顺序、列表操作和用于检查和修改逻辑数据库的高阶谓词。
Much of our discussion was driven by an examination of the principal logic language, Prolog, which we used to illustrate clauses and terms, resolution and unification, search/execution order, list manipulation, and high-order predicates for inspection and modification ofthe logic database.
与命令式和函数式编程一样,逻辑编程与构造性证明有关。但是,命令式或函数式程序在某种意义上是一种证明(证明能够从输入生成输出),而逻辑程序则是一组公理,计算机试图从中构造证明。命令式和函数式编程分别提供了图灵机和 lambda 演算的全部功能(忽略硬件对算术精度、磁盘和内存空间等的限制),而 Prolog 提供的分解定理证明的通用性则不够充分,这是为了提高时间和空间效率。同时,Prolog 扩展了其正式对应部分,使其具有真正的算术、I/O、命令式控制流和高阶谓词,以便进行自我检查和修改。
Like imperative and functional programming, logic programming is related to constructive proofs. But where an imperative or functional program in some sense is a proof (of the ability to generate outputs from inputs), a logic program is a set of axioms from which the computer attempts to construct a proof. And where imperative and functional programming provide the full power of Turing machines and lambda calculus, respectively (ignoring hardware-imposed limits on arithmetic precision, disk and memory space, etc.), Prolog provides less than the full generality of resolution theorem proving, in the interests of time and space efficiency. At the same time, Prolog extends its formal counterpart with true arithmetic, I/O, imperative control flow, and higher-order predicates for self-inspection and modification.
与 Lisp/Scheme 一样,Prolog 大量使用列表,主要是因为它们可以轻松地逐步构建,而无需将分配和修改状态作为单独的操作。与 Lisp/Scheme 一样(但与 ML 及其后代不同),Prolog 是同形的:程序看起来像普通的数据结构,并且可以即时创建、修改和执行。
Like Lisp/Scheme, Prolog makes heavy use of lists, largely because they can easily be built incrementally, without the need to allocate and then modify state as separate operations. And like Lisp/Scheme (but unlike ML and its descendants), Prolog is homoiconic: programs look like ordinary data structures, and can be created, modified, and executed on the fly.
正如我们在第 1 章中强调的那样,不同的计算模型有不同的吸引力。命令式程序更紧密地反映了底层硬件,并且可以更轻松地“调整”以获得高性能。纯函数式程序避免了副作用的语义复杂性,并且已被证明对于操作符号(非数字)数据特别有用。逻辑程序具有高度声明性的语义并强调统一,非常适合强调关系和搜索的问题。同时,它们不强调控制流可能会导致效率低下。在目前的技术水平下,计算机在处理低级细节(例如,指令调度)的能力上已经超越了人类,但人类在发明好的算法方面仍然更胜一筹。
As we stressed in Chapter 1, different models of computing are appealing in different ways. Imperative programs more closely mirror the underlying hardware, and can more easily be “tweaked” for high performance. Purely functional programs avoid the semantic complexity of side effects, and have proved particularly handy for the manipulation of symbolic (nonnumeric) data. Logic programs, with their highly declarative semantics and their emphasis on unification, are well suited to problems that emphasize relationships and search. At the same time, their de-emphasis of control flow can lead to inefficiency. At the current state of the art, computers have surpassed people in their ability to deal with low-level details (e.g., of instruction scheduling), but people are still better at inventing good algorithms.
正如我们在第 1 章中强调的那样,语言类别之间的界限通常非常模糊。Prolog 的回溯搜索与 Icon 中生成器的执行非常相似。Prolog 中的统一类似于(但比)ML 和 Haskell 的模式匹配功能。(统一也用于 ML 和 Haskell 中的类型检查,以及 C++ 中的模板实例化,但这些都是编译时活动。)
As we also stressed in Chapter 1, the borders between language classes are often very fuzzy. The backtracking search of Prolog strongly resembles the execution of generators in Icon. Unification in Prolog resembles (but is more powerful than) the pattern-matching capabilities of ML and Haskell. (Unification is also used for type checking in ML and Haskell, and for template instantiation in C++, but those are compile-time activities.)
纯函数式或基于逻辑的编程有很多优点。虽然大多数 Scheme 和 Prolog 程序都使用了一些命令式语言特性,但这些特性往往是造成大量程序错误的原因。同时,似乎有些编程任务(例如交互式 I/O)几乎不可能不产生副作用。
There is much to be said for programming in a purely functional or logic-based style. While most Scheme and Prolog programs make some use of imperative language features, those features tend to be responsible for a disproportionate share of program bugs. At the same time, there seem to be programming tasks—interactive I/O, for example—that are almost impossible to accomplish without side effects.
12.1 从示例 12.17开头的子句开始,使用归结法(如示例 12.3所示)以两种不同的方式表明存在从a到e的路径。
12.1 Starting with the clauses at the beginning of Example 12.17, use resolution (as illustrated in Example 12.3) to show, in two different ways, that there is a path from a to e.
12.2 解决Prolog中的练习6.22。
12.2 Solve Exercise 6.22 in Prolog.
12.3考虑 图 1.2中的Prolog gcd程序。这个程序能“反向”运行,也能“正向”运行吗?(给定整数d和n,能否使用它生成一个整数序列m,使得gcd ( n,m )= d?)解释你的答案。
12.3 Consider the Prolog gcd program in Figure 1.2. Does this program work “backward” as well as forward? (Given integers d and n, can you use it to generate a sequence of integers m such that gcd (n, m) = d?) Explain your answer.
12.4本着 示例 11.20的精神,编写一个 Prolog 程序,利用回溯来模拟非确定性有限自动机的执行。
12.4 In the spirit of Example 11.20, write a Prolog program that exploits backtracking to simulate the execution of a nondeterministic finite automaton.
12.5 证明归结是可交换和可结合的。具体来说,如果A、B和C是 Horn 子句,则证明 ( A ⊕ B ) = ( B ⊕ A ) 并且 ( ( A ⊕ B ) ⊕ C ) = ( A ⊕ ( B ⊕ C ) ),其中 ⊕ 表示归结。一定要思考由于统一而实例化的变量会发生什么。
12.5 Show that resolution is commutative and associative. Specifically, if A, B, and C are Horn clauses, show that (A ⊕ B) = (B ⊕ A) and that ((A ⊕ B) ⊕ C) = (A ⊕ (B ⊕ C)), where ⊕ indicates resolution. Be sure to think about what happens to variables that are instantiated as a result of unification.
12.6 在示例 12.8中,查询?- classmates(jane_doe, X)将成功三次:两次X = jane_doe,一次X = ajit_chandra。说明如何修改classmates(X, Y)规则,以便学生不被视为自己的同学。
12.6 In Example 12.8, the query ?- classmates(jane_doe, X) will succeed three times: twice with X = jane_doe and once with X = ajit_chandra. Show how to modify the classmates(X, Y) rule so that a student is not considered a classmate of himself or herself.
12.7 修改示例 12.17,使得对于任意已经实例化的X和Y ,目标path(X, Y)不会成功超过一次,即使从X到Y有多条路径。
12.7 Modify Example 12.17 so that the goal path(X, Y), for arbitrary already-instantiated X and Y, will succeed no more than once, even if there are multiple paths from X to Y.
12.8 仅使用\+ (无切入),修改第 12.2.5 节中的井字游戏示例,使其从给定的棋盘位置仅生成一个候选走法。您的解决方案与基于切入的解决方案(示例 12.22)相比如何?
12.8 Using only \+ (no cuts), modify the tic-tac-toe example of Section 12.2.5 so it will generate only one candidate move from a given board position. How does your solution compare to the cut-based one (Example 12.22)?
12.9证明 例 12.19中的论断:井字游戏没有必胜策略,即任何一个玩家都可以强行平局。
12.9 Prove the claim, made in Example 12.19, that there is no winning strategy in tic-tac-toe—that either player can force a draw.
12.10证明 例 12.19中的井字游戏策略是最优的(尽可能战胜不完美的对手,否则打平),或者给出一个反例。
12.10 Prove that the tic-tac-toe strategy of Example 12.19 is optimal (wins against an imperfect opponent whenever possible, draws otherwise), or give a counterexample.
12.11 从图 12.4中的井字游戏程序开始,画一个有向无环图,其中每个子句都是一个节点,从A到B 的弧表示,对于正确性或效率而言,程序中A先于B非常重要。(不要画任何其他弧。)图的任何拓扑排序都应构成程序的同等高效版本。(现有程序是其中之一吗?)
12.11 Starting with the tic-tac-toe program of Figure 12.4, draw a directed acyclic graph in which every clause is a node and an arc from A to B indicates that it is important, either for correctness or efficiency, that A come before B in the program. (Do not draw any other arcs.) Any topological sort of your graph should constitute an equally efficient version of the program. (Is the existing program one of them?)
12.12编写 Prolog 规则来定义 成员谓词的一个版本,该版本将在回溯期间生成列表的所有成员,但不生成重复项。请注意,示例 12.20中的cut 和基于\+ 的版本将还不够;当被要求寻找未实例化的成员时,他们只能找到列表的头部。
12.12 Write Prolog rules to define a version of the member predicate that will generate all members of a list during backtracking, but without generating duplicates. Note that the cut and \+ based versions of Example 12.20 will not suffice; when asked to look for an uninstantiated member, they find only the head of the list.
12.13 使用Prolog 的子句谓词来实现call谓词(假设它不是内置的)。您不需要实现 Prolog 的所有内置谓词;特别是,您可以忽略各种命令式控制流机制和数据库操纵器。通过将数据库作为call的显式参数来扩展您的代码,从而有效地生成一个元循环解释器。
12.13 Use the clause predicate of Prolog to implement the call predicate (pretend that it isn't built in). You needn't implement all of the built-in predicates of Prolog; in particular, you may ignore the various imperative control-flow mechanisms and database manipulators. Extend your code by making the database an explicit argument to call, effectively producing a metacircular interpreter.
12.14 使用Prolog 的子句谓词编写一个谓词call_bfs,尝试广度优先地满足目标。(提示:您需要保留一个尚未实现的子目标队列,每个子目标都由一个捕获回溯替代方案的堆栈表示。)
12.14 Use the clause predicate of Prolog to write a predicate call_bfs that attempts to satisfy goals breadth-first. (Hint: You will want to keep a queue of yet-to-be-pursued subgoals, each of which is represented by a stack that captures backtracking alternatives.)
12.15用 Prolog 编写一个(基于列表的)插入排序算法。在 C 语言中,使用数组的实现如下:void insert_sort(int A[], int N) { int i, j, t; for (i = 1; i < N; i++) { t = A[i]; for (j = i; j > 0; j--) { if (t >= A[j−1]) break; A[j] = A[j−1]; } A[j] = t; } }
12.15 Write a (list-based) insertion sort algorithm in Prolog. Here's what it looks like in C, using arrays:
void insertion_sort(int A[], int N)
{
int i, j, t;
for (i = 1; i < N; i++) {
t = A[i];
for (j = i; j > 0; j--) {
if (t >= A[j−1]) break;
A[j] = A[j−1];
}
A[j] = t;
}
}
12.16 快速排序适用于大型列表,但对于短列表,其开销比插入排序高。用 Prolog 编写一个排序算法,最初使用快速排序,但对于元素不超过 15 个的子列表,切换到插入排序(如上一个练习中所定义)。(提示:您可以在分区操作期间计算元素的数量。)
12.16 Quicksort works well for large lists, but has higher overhead than insertion sort for short lists. Write a sort algorithm in Prolog that uses quicksort initially, but switches to insertion sort (as defined in the previous exercise) for sublists of 15 or fewer elements. (Hint: You can count the number of elements during the partition operation.)
12.17 编写一个 Prolog 排序程序,保证在最坏情况下花费O ( n log n ) 时间。(提示:尝试归并排序;几乎可以在任何算法或数据结构文本中找到描述。)
12.17 Write a Prolog sorting routine that is guaranteed to take O(n log n) time in the worst case. (Hint: Try merge sort; a description can be found in almost any algorithms or data structures text.)
12.18 考虑与 Prolog 解释器的以下交互:?- Y = X,X = foo(X)。Y = foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo (foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo (foo(foo(foo(foo(foo(…这里发生了什么?为什么解释器会陷入无限循环?你能想到任何情况(大概不需要输出)
像这样的结构有什么用处?如果没有用,您能否建议 Prolog 解释器如何实现检查以禁止其创建?这些检查的成本有多高?您认为这些成本是否合理?
12.18 Consider the following interaction with a Prolog interpreter:
?- Y = X, X = foo(X).
Y = foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(
foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(
foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(
foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(
foo(foo(foo(foo(foo(foo(…
What is going on here? Why does the interpreter fall into an infinite loop? Can you think of any circumstances (presumably not requiring output) in which a structure like this one would be useful? If not, can you suggest how a Prolog interpreter might implement checks to forbid its creation? How expensive would those checks be? Would the cost in your opinion be justified?
12.19–12.21 更深入。
12.19–12.21 In More Depth.
12.22 了解 Prolog 和其他逻辑语言的替代搜索策略。正向链接求解器如何工作?智能混合策略的前景如何?
12.22 Learn about alternative search strategies for Prolog and other logic languages. How do forward chaining solvers work? What are the prospects for intelligent hybrid strategies?
12.23 1982 年至 1992 年间,日本政府在逻辑编程方面投入了大量资金。研究由日本国际贸易和工业部 (MITI) 管理的第五代项目。它的目标是什么?实现了什么?没有实现什么?目标和结果与 Prolog 的联系有多紧密?我们今天可以从该项目中学到什么教训?
12.23 Between 1982 and 1992 the Japanese government invested large sums of money in logic programming. Research the Fifth Generation project, administered by the Japanese Ministry of International Trade and Industry (MITI). What were its goals? What was achieved? What was not? How tightly were the goals and outcomes tied to Prolog? What lessons can we learn from the project today?
12.24 提前阅读第 14 章并了解 XSLT,这是一种用于处理以 XML(扩展标记语言,其中最新的网页标准 XHTML 就是一个例子)表示的数据的语言。XSLT 通常被描述为声明式的。它是基于逻辑的吗?它在表达能力、抽象级别和执行效率方面与 Prolog 相比如何?
12.24 Read ahead to Chapter 14 and learn about XSLT, a language used to manipulate data represented in XML, the extended markup language (of which XHTML, the latest standard for web pages, is an example). XSLT is generally described as declarative. Is it logic based? How does it compare to Prolog in expressive power, level of abstraction, and execution efficiency?
12.25 对数据库查询语言 SQL 重复上一个问题(有关介绍,请在您最喜欢的互联网搜索引擎中输入“SQL 教程”)。
12.25 Repeat the previous question for SQL, the database query language (for an introduction, type “SQL tutorial” into your favorite Internet search engine).
12.26 像 Microsoft Excel 这样的电子表格有时被描述为声明式编程。这公平吗?忽略 Visual Basic 宏之类的扩展,定义单元格之间关系的能力是否提供了图灵完备的表达能力?将执行模型与 Prolog 的执行模型进行比较。如何确定单元格的更新顺序?数据可以像在 Prolog 中一样“双向”推送吗?
12.26 Spreadsheets like Microsoft Excel are sometimes characterized as declarative programming. Is this fair? Ignoring extensions like Visual Basic macros, does the ability to define relationships among cells provide Turing complete expressive power? Compare the execution model to that of Prolog. How is the order of update for cells determined? Can data be pushed “both ways,” as they can in Prolog?
12.27–12.30 更深入。
12.27–12.30 In More Depth.
逻辑编程的根源在于自动定理证明。大部分理论基础由 Horn 在 20 世纪 50 年代早期奠定 [ Hor51 ],Robinson 在 20 世纪 60 年代早期奠定 [ Rob65 ]。计算领域的突破发生在20 世纪 70 年代初,法国艾克斯-马赛大学的 Colmeraurer 和 Roussel 与苏格兰爱丁堡大学的 Kowalski 及其同事开发了 Prolog 的初始版本。Robinson [ Rob83 ] 讲述了该语言的早期历史。Lloyd [ Llo87 ]介绍了理论基础。
Logic programming has its roots in automated theorem proving. Much of the theoretical groundwork was laid by Horn in the early 1950s [Hor51], and by Robinson in the early 1960s [Rob65]. The breakthrough for computing came in the early 1970s, when Colmeraurer and Roussel at the University of Aix–Marseille in France and Kowalski and his colleagues at the University of Edinburgh in Scotland developed the initial version of Prolog. The early history of the language is recounted by Robinson [Rob83]. Theoretical foundations are covered by Lloyd [Llo87].
Prolog 最初是为自然语言处理研究而设计的,但很快人们就发现它可以用作通用语言。此后,Prolog 已发展出多个版本。这里描述的是广泛使用的爱丁堡方言。ISO 标准 [ Int95 ] 与之类似。
Prolog was originally intended for research in natural language processing, but it soon became apparent that it could serve as a general-purpose language. Several versions of Prolog have since evolved. The one described here is the widely used Edinburgh dialect. The ISO standard [Int95] is similar.
已经开发了几种其他逻辑语言,但没有一种在受欢迎程度上能与 Prolog 相媲美。OPS5 [ BFKM86 ] 使用了前向链接。Godel [ HL94 ] 包括模块、强类型、更丰富的逻辑运算符以及增强的执行顺序控制。Parlog 是并行 Prolog 方言;我们将在13.4.5 节中简要提到它。Mercury [ SHC96 ] 采用了 ML 系列函数式语言的各种特性,包括静态类型推断、类似 monad 的 I/O、高阶谓词、闭包、柯里化和 lambda 表达式。它是编译型的,而不是解释型的,并且要求程序员指定谓词参数的模式(in、out)。
Several other logic languages have been developed, though none rivaled Prolog in popularity. OPS5 [BFKM86] used forward chaining. Godel [HL94] includes modules, strong typing, a richer variety of logical operators, and enhanced control of execution order. Parlog is a parallel Prolog dialect; we will mention it briefly in Section 13.4.5. Mercury [SHC96] adopts a variety of features from ML-family functional languages, including static type inference, monad-like I/O, higher-order predicates, closures, currying, and lambda expressions. It is compiled, rather than interpreted, and requires the programmer to specify modes (in, out) for predicate arguments.
源自 Datalog [ Ull85 ] [ UW08,第 4.2至4.4节] 的数据库查询语言是使用正向链接实现的。CLP(约束逻辑编程)及其变体主要基于 Prolog,但采用更通用的约束满足机制来代替统一 [ JM94 ]。逻辑编程协会的网址为www.cs.nmsu.edu/ALP/。
Database query languages stemming from Datalog [Ull85] [UW08, Secs. 4.2–4.4] are implemented using forward chaining. CLP (Constraint Logic Programming) and its variants are largely based on Prolog, but employ a more general constraint-satisfaction mechanism in place of unification [JM94]. The Association for Logic Programming can be found on-line at www.cs.nmsu.edu/ALP/.
本文的大部分内容都隐含地集中在顺序程序上:具有单个活动执行上下文的程序。正如我们在第 6 章中看到的,顺序是命令式编程的基础。它在声明式编程中也往往是隐含的,部分原因是实用的函数式和逻辑语言通常包含一些命令式特性,部分原因是人们倾向于开发声明式程序的命令式实现和心理模型(应用顺序减少、带回溯的后向链接),即使语言语义不需要这样的模型。
The bulk of this text has focused, implicitly, on sequential programs: programs with a single active execution context. As we saw in Chapter 6, sequentially is fundamental to imperative programming. It also tends to be implicit in declarative programming, partly because practical functional and logic languages usually include some imperative features, and partly because people tend to develop imperative implementations and mental models of declarative programs (applicative order reduction, backward chaining with backtracking), even when language semantics do not require such a model.
相比之下,如果一个程序可能有多个活动执行上下文(多个“控制线程”),则称该程序是并发的。并发至少有三个重要动机:
By contrast, a program is said to be concurrent if it may have more than one active execution context—more than one “thread of control.” Concurrency has at least three important motivations:
1. 捕捉问题的逻辑结构。许多程序(尤其是服务器和图形应用程序)必须同时跟踪多个基本独立的“任务”。构造此类程序的最简单、最合乎逻辑的方法通常是用单独的控制线程表示每个任务。我们在讨论协程(第9.5 节)和事件(第 9.6 节)时提到过这种“多线程”结构;我们将在13.1.1 节中再次讨论它。
1. To capture the logical structure of a problem. Many programs, particularly servers and graphical applications, must keep track of more than one largely independent “task” at the same time. Often the simplest and most logical way to structure such a program is to represent each task with a separate thread of control. We touched on this “multithreaded” structure when discussing coroutines (Section 9.5) and events (Section 9.6); we will return to it in Section 13.1.1.
2. 利用并行硬件提高速度。多处理器(或处理器中的多个内核)长期以来一直是高端服务器和超级计算机的必备配置,如今已在台式机、笔记本电脑和移动设备中随处可见。为了有效使用这些内核,通常必须在编写(或重写)程序时考虑并发性。
2. To exploit parallel hardware, for speed. Long a staple of high-end servers and supercomputers, multiple processors (or multiple cores within a processor) have become ubiquitous in desktop, laptop, and mobile devices. To use these cores effectively, programs must generally be written (or rewritten) with concurrency in mind.
3. 应对物理分布。在互联网或本地计算机组上运行的应用程序本质上是并发的。许多嵌入式应用程序也是如此:例如,现代汽车的控制系统可能横跨遍布整个车辆的数十个处理器。
3. To cope with physical distribution. Applications that run across the Internet or a more local group of machines are inherently concurrent. So are many embedded applications: the control systems of a modern automobile, for example, may span dozens of processors spread throughout the vehicle.
一般来说,我们使用“并发”这个词来描述任何可能同时进行两个或多个任务(在执行过程中的不可预测点)的系统。根据这个定义,协程不是并发的,因为在任何给定时间,除一个任务外,其他所有任务都会停止在一个已知位置。如果多个任务可以同时在物理上处于活动状态,则并发系统是并行的;这需要多个处理器。这种区别纯粹是实现和性能问题:从语义的角度来看,真正的并行性和在不可预测的时间在任务之间切换的系统的“准并行性”之间没有区别。如果并行系统的处理器与现实世界中彼此物理分离的人或设备相关联,则该系统是分布式的。根据这些定义,“并发”适用于上述所有三个动机。“并行”适用于第二和第三个动机;“分布式”仅适用于第三个。
In general, we use the word concurrent to characterize any system in which two or more tasks may be underway (at an unpredictable point in their execution) at the same time. Under this definition, coroutines are not concurrent, because at any given time, all but one of them is stopped at a well-known place. A concurrent system is parallel if more than one task can be physically active at once; this requires more than one processor. The distinction is purely an implementation and performance issue: from a semantic point of view, there is no difference between true parallelism and the “quasiparallelism” of a system that switches between tasks at unpredictable times. A parallel system is distributed if its processors are associated with people or devices that are physically separated from one another in the real world. Under these definitions, “concurrent” applies to all three motivations above. “Parallel” applies to the second and third; “distributed” applies to only the third.
本章将重点讨论并发性和并行性。自 2005 年左右以来,随着多核处理器的普及,并行性已成为一个紧迫的问题。我们将很少有机会涉及分布式。虽然语言是为分布式计算而设计的,但大多数分布式系统在每个联网处理器上运行单独的程序,并使用消息传递库例程在它们之间进行通信。
We will focus in this chapter on concurrency and parallelism. Parallelism has become a pressing concern since 2005 or so, with the proliferation of multicore processors. We will have less occasion to touch on distribution. While languages have been designed for distributed computing, most distributed systems run separate programs on every networked processor, and use message-passing library routines to communicate among them.
我们首先概述了在现代程序中使用并行性的方式。我们的概述将涉及并发的动机(即使在单处理器上)和竞争概念,这是并发程序复杂性的主要来源。我们还将简要介绍现代多核和多处理器机器的架构特征。在第 13.2 节中,我们将考虑共享内存和消息传递并发模型之间的对比,以及语言和基于库的实现之间的对比。基于协程,我们解释了语言或库如何创建和调度线程。第 13.3 节重点介绍共享内存同步的低级机制。第 13.4 节将讨论扩展到语言级构造。第 13.5 节(主要在配套站点上)讨论了并发的消息传递模型。
We begin our study with an overview of the ways in which parallelism may be used in modern programs. Our overview will touch on the motivation for concurrency (even on uniprocessors) and the concept of races, which are the principal source of complexity in concurrent programs. We will also briefly survey the architectural features of modern multicore and multiprocessor machines. In Section 13.2 we consider the contrast between shared-memory and message-passing models of concurrency, and between language and library-based implementations. Building on coroutines, we explain how a language or library can create and schedule threads. Section 13.3 focuses on low-level mechanisms for shared-memory synchronization. Section 13.4 extends the discussion to language-level constructs. Message-passing models of concurrency are considered in Section 13.5 (mostly on the companion site).
并发并不是一个新概念。大部分理论基础是在 20 世纪 60 年代奠定的,Algol 68 包含并发编程功能。然而,人们对并发的广泛兴趣是一个相对较新的现象;它部分源于低成本多核和多处理器机器的出现,部分源于图形、多媒体和基于 Web 的应用程序的激增,所有这些都自然地由并发控制线程表示。
Concurrency is not a new idea. Much of the theoretical groundwork was laid in the 1960s, and Algol 68 includes concurrent programming features. Widespread interest in concurrency is a relatively recent phenomenon, however; it stems in part from the availability of low-cost multicore and multiprocessor machines, and in part from the proliferation of graphical, multimedia, and web-based applications, all of which are naturally represented by concurrent threads of control.
并行性出现在现代计算机系统的每个层面。在电路和门级,信号可以同时沿着数千个连接传播,因此并行性相对容易利用。当我们首先向上移动到处理器和核心,然后移动到在其上运行的多层软件时,粒度 并行性(任务的大小和复杂性)在每个级别上都在增加,并且越来越难以确定每个任务应该做什么工作以及任务应该如何协调。
Parallelism arises at every level of a modern computer system. It is comparatively easy to exploit at the level of circuits and gates, where signals can propagate down thousands of connections at once. As we move up first to processors and cores, and then to the many layers of software that run on top of them, the granularity of parallelism—the size and complexity of tasks—increases at every level, and it becomes increasingly difficult to figure out what work should be done by each task and how tasks should coordinate.
40 年来,微架构研究主要致力于寻找更多更好的方法来利用机器语言程序中可用的指令级并行性 (ILP)。正如我们在第 5 章中看到的,深度超标量流水线和积极推测的结合使现代处理器能够跟踪数百条“正在运行”的指令之间的依赖关系,在其中数十条指令上取得进展,并在每个周期内完成几条指令。在世纪之交后不久,很明显已经达到了极限:传统程序中根本没有更多的指令级并行性可用。
For 40 years, microarchitectural research was largely devoted to finding more and better ways to exploit the instruction-level parallelism (ILP) available in machine language programs. As we saw in Chapter 5, the combination of deep, superscalar pipelines and aggressive speculation allows a modern processor to track dependences among hundreds of “in-flight” instructions, make progress on scores of them, and complete several in every cycle. Shortly after the turn of the century, it became apparent that a limit had been reached: there simply wasn't any more instruction-level parallelism available in conventional programs.
在下一个更高的粒度级别,所谓的向量并行性可用于对非常大的数据集的每个元素重复执行操作的程序。从 20 世纪 60 年代末到 90 年代初,利用这种并行性的处理器是超级计算机的主流形式。它们的遗产在主流处理器的向量指令(例如 x86 指令集的 MMX、SSE 和 AVX 扩展)和现代图形处理单元 (GPU) 中得以延续,其峰值性能可以超过典型 CPU(中央处理单元 - 传统核心)的 100 多倍。
At the next higher level of granularity, so-called vector parallelism is available in programs that perform operations repeatedly on every element of a very large data set. Processors designed to exploit this parallelism were the dominant form of supercomputer from the late 1960s through the early 1990s. Their legacy lives on in the vector instructions of mainstream processors (e.g., the MMX, SSE, and AVX extensions to the x86 instruction set), and in modern graphical processing units (GPUs), whose peak performance can exceed that of the typical CPU (central processing unit—a conventional core) by a factor of more than 100.
不幸的是,向量并行只出现在某些类型的程序中。鉴于 ILP 的终结,以及散热对时钟频率的限制(第 C-5.4.4 节),当今的通用计算必须从多核处理器中获得性能改进,而多核处理器需要更粗粒度的线程级并行。因此,转向多核意味着编程性质的根本转变:并行曾经是一个很大程度上不可见的实现细节,现在必须将其明确写入高级程序结构中。
Unfortunately, vector parallelism arises in only certain kinds of programs. Given the end of ILP, and the limits on clock frequency imposed by heat dissipation (Section C-5.4.4), general-purpose computing today must obtain its performance improvements from multicore processors, which require coarser-grain thread-level parallelism. The move to multicore has thus entailed a fundamental shift in the nature of programming: where parallelism was once a largely invisible implementation detail, it must now be written explicitly into high-level program structure.
在当今的多核机器上,不同类型的程序员需要理解不同细节级别的并发性,并以不同的方式使用它。
On today's multicore machines, different kinds of programmers need to understand concurrency at different levels of detail, and use it in different ways.
最简单、最抽象的情况是使用“黑盒”并行库。例如,排序例程或线性代数包可以并行执行,而其调用者无需了解如何执行。在数据库世界中,以 SQL(结构化查询语言)表达的查询通常也是并行执行的。Microsoft 的 .NET Framework 包含一种语言集成查询机制 (LINQ),允许使用程序数据结构进行数据库样式的查询,同样具有“幕后”并行性。
The simplest, most abstract case arises when using “black box” parallel libraries. A sorting routine or a linear algebra package, for example, may execute in parallel without its caller needing to understand how. In the database world, queries expressed in SQL (Structured Query Language) often execute in parallel as well. Microsoft's .NET Framework includes a Language-Integrated Query mechanism (LINQ) that allows database-style queries to be made of program data structures, again with parallelism “under the hood.”
同步的最常见目的是使某些指令序列(称为临界区)看起来具有原子性— — 从所有其他线程的角度来看是“同时”发生的。在我们的示例中,临界区是加载、增量和存储。使序列具有原子性的最常见方法是使用互斥锁,我们在序列的第一条指令之前获取该锁,并在最后一条指令之后释放该锁。我们将在 13.3.1和13.3.5节中研究锁。在13.3.2和13.4.4节中,我们还将考虑不使用锁实现原子性的机制。
The most common purpose of synchronization is to make some sequence of instructions, known as a critical section, appear to be atomic—to happen “all at once” from the point of view of every other thread. In our example, the critical section is a load, an increment, and a store. The most common way to make the sequence atomic is with a mutual exclusion lock, which we acquire before the first instruction of the sequence and release after the last. We will study locks in Sections 13.3.1 and 13.3.5. In Sections 13.3.2 and 13.4.4 we will also consider mechanisms that achieve atomicity without locks.
在较低的抽象层次上,专业程序员可能需要充分了解硬件和运行时系统才能实现同步机制。本章应该传达这些问题的意识,但在这一层次上的完整处理超出了本书的范围。
At lower levels of abstraction, expert programmers may need to understand hardware and run-time systems in sufficient detail to implement synchronization mechanisms. This chapter should convey a sense of the issues, but a full treatment at this level is beyond the scope of the current text.
我们研究并发的第一个动机——捕捉某些应用程序的逻辑结构——在前面的章节中已经出现过几次。在 C-8.7.1 节中,我们注意到交互式 I/O 经常会中断当前程序的执行。例如,在视频游戏中,我们必须处理键击、鼠标或操纵杆移动,同时不断更新屏幕上的图像。构建此类程序的标准方法(如9.6.2 节所述)是在单独的控制线程中执行输入处理程序,该线程与负责更新屏幕的一个或多个线程共存。在9.5 节中,我们考虑了一个屏幕保护程序,该程序使用协同程序将文件系统的“健全性检查”与屏幕上运动图像的更新交错进行。我们还考虑了离散事件模拟,它使用协同程序来表示某个现实世界系统的活动实体。
Our first motivation for concurrency—to capture the logical structure of certain applications—has arisen several times in earlier chapters. In Section C-8.7.1 we noted that interactive I/O must often interrupt the execution of the current program. In a video game, for example, we must handle keystrokes and mouse or joystick motions while continually updating the image on the screen. The standard way to structure such a program, as described in Section 9.6.2, is to execute the input handlers in a separate thread of control, which coexists with one or more threads responsible for updating the screen. In Section 9.5, we considered a screen saver program that used coroutines to interleave “sanity checks” on the file system with updates to a moving picture on the screen. We also considered discrete-event simulation, which uses coroutines to represent the active entities of some real-world system.
离散事件模拟的语义要求事件在固定的时间点以原子方式发生。协程提供了一种自然的实现,因为它们每次只执行一个:只要我们从不在原子操作中切换协程,一切就都很好。然而,在我们的其他示例中——实际上在大多数“自然并发”程序中——不需要协程语义。通过将并发任务分配给线程而不是协程,我们承认如果有多个核心可用,这些任务可以并行进行。我们还将确定哪个线程应该在何时运行的责任从程序员转移到语言实现。作为回报,我们放弃了任何琐碎的原子性概念。
The semantics of discrete-event simulation require that events occur atomically at fixed points in time. Coroutines provide a natural implementation, because they execute one at a time: so long as we never switch coroutines in the middle ofa to-be-atomic operation, all will be well. In our other examples, however— and indeed in most “naturally concurrent” programs—there is no need for coroutine semantics. By assigning concurrent tasks to threads instead of to coroutines, we acknowledge that those tasks can proceed in parallel if more than one core is available. We also move responsibility for figuring out which thread should run when from the programmer to the language implementation. In return, we give up any notion of trivial atomicity.
使用多个线程可确保相对较快的操作(例如,显示文本)不会等待较慢的操作(例如,显示大图像)。每当一个线程阻塞(等待消息或 I/O)时,运行时或操作系统都会自动切换核心上的执行以运行不同的线程。在抢占式线程包中,这些上下文切换也会在其他时间发生,以防止任何一个线程占用处理器资源。任何记得早期更连续的浏览器的读者都会欣赏多线程在感知性能和响应能力方面带来的差异,即使在单核机器上也是如此。
The use of many threads ensures that comparatively fast operations (e.g., display of text) do not wait for slow operations (e.g., display of large images). Whenever one thread blocks (waits for a message or I/O), the run-time or operating system will automatically switch execution on the core to run a different thread. In a preemptive thread package, these context switches will occur at other times as well, to prevent any one thread from hogging processor resources. Any reader who remembers early, more sequential browsers will appreciate the difference that multithreading makes in perceived performance and responsiveness, even on a single-core machine.
调度循环的主要问题(除了细分任务和保存状态的复杂性之外)是它隐藏了程序的算法结构。如果不是因为我们必须在每个延迟操作后返回调度循环的顶部,那么每个不同的任务(检索页面、渲染图像、浏览嵌套菜单)都可以用标准控制流机制优雅地描述。实际上,调度循环将程序“彻底颠覆”,使任务的管理变得明确,任务内的控制流变得隐含。由此产生的复杂性类似于我们在尝试使用迭代器对象枚举递归集(见第 6.5.3 节),情况只会更糟。与真正的迭代器一样,线程包将程序“翻转过来”,使任务(线程)的管理隐式化,并使线程内的控制流显式化。
The principal problem with a dispatch loop—beyond the complexity of subdividing tasks and saving state—is that it hides the algorithmic structure of the program. Every distinct task (retrieving a page, rendering an image, walking through nested menus) could be described elegantly with standard control-flow mechanisms, if not for the fact that we must return to the top of the dispatch loop at every delay-inducing operation. In effect, the dispatch loop turns the program “inside out,” making the management of tasks explicit and the control flow within tasks implicit. The resulting complexity is similar to what we encountered when trying to enumerate a recursive set with iterator objects in Section 6.5.3, only worse. Like true iterators, a thread package turns the program “right side out,” making the management of tasks (threads) implicit and the control flow within threads explicit.
并行计算机硬件种类繁多。分布式系统(我们将其视为在不同机器上运行的不同程序之间的交互)可能大到互联网,也可能小到手机的组件。并行但非分布式系统(我们将其视为在一台机器上运行的单个程序)可能仍然非常庞大。例如,中国的天河二号超级计算机拥有 300 多万个核心,功耗超过 17 兆瓦,占地面积 720 平方米(约五分之一英亩)。
Parallel computer hardware is enormously diverse. A distributed system—one that we think of in terms of interactions among separate programs running on separate machines—maybe as large as the Internet, or as small as the components of a cell phone. A parallel but nondistributed system—one that we think of in terms of a single program running on a single machine—may still be very large. China's Tianhe-2 supercomputer, for example, has more than 3 million cores, consumes over 17 MW of power, and occupies 720 square meters of floor space (about a fifth of an acre).
从历史上看,大多数并行但非分布式机器都是同质的——它们的处理器都是相同的。近年来,许多机器都增加了可编程 GPU,首先是作为单独的处理器,最近则是作为单个处理器芯片的单独部分。虽然 GPU 的内核在内部是同质的,但它们与典型 CPU 的内核非常不同,从而导致全局异构系统。未来的系统可能还会有许多其他类型的内核,每个内核都专用于特定类型的程序或程序组件。
Historically, most parallel but nondistributed machines were homogeneous— their processors were all identical. In recent years, many machines have added programmable GPUs, first as separate processors, and more recently as separate portions of a single processor chip. While the cores of a GPU are internally homogeneous, they are very different from those of the typical CPU, leading to a globally heterogeneous system. Future systems may have cores of many other kinds as well, each specialized to particular kinds of programs or program components.
在理想情况下,编程语言和运行时会在合适的时间将程序片段映射到合适的内核,但这种自动化仍然是一个研究目标。截至 2015 年,想要利用 GPU 的程序员会用 OpenCL 或 CUDA 等专用语言编写适当部分的代码,这些语言强调重复操作而不是向量。然后,在 CPU 上运行的主程序会将生成的“内核”明确发送到 GPU。
In an ideal world, programming languages and runtimes would map program fragments to suitable cores at suitable times, but this sort of automation is still very much a research goal. As of 2015, programmers who want to make use of the GPU write appropriate portions of their code in special-purpose languages like OpenCL or CUDA, which emphasize repetitive operations over vectors. A main program, running on the CPU, then ships the resulting “kernels” to the GPU explicitly.
在本章的剩余部分,我们将集中讨论同构机器的线程级并行性。对于这些机器,许多最重要的架构问题涉及内存系统。在某些机器中,所有物理内存都可以供每个核心访问,并且硬件保证每个写入在任何地方都快速可见。在另一个极端,一些机器将主内存划分给处理器,迫使核心通过某种单独的消息传递机制进行交互。在中间设计中,一些机器以非一致的方式共享内存,只有当两个核心都明确刷新了缓存时,一个核心上的写入才对另一个核心可见。
In the remainder of this chapter, we will concentrate on thread-level parallelism for homogeneous machines. For these, many of the most important architectural questions involve the memory system. In some machines, all of physical memory is accessible to every core, and the hardware guarantees that every write is quickly visible everywhere. At the other extreme, some machines partition main memory among processors, forcing cores to interact through some separate message-passing mechanism. In intermediate designs, some machines share memory in a noncoherent fashion, making writes on one core visible to another only when both have explicitly flushed their caches.
从语言或库实现的角度来看,共享内存和消息传递硬件之间的主要区别在于,消息通常需要连接两端的核心积极参与:一个核心发送,另一个核心接收。在共享内存机器上,一个核心可以读取和写入远程内存,而无需任何其他核心的协助。
From the point of view of language or library implementation, the principal distinction between shared-memory and message-passing hardware is that messages typically require the active participation of cores at both ends of the connection: one to send, the other to receive. On a shared-memory machine, a core can read and write remote memory without any other core's assistance.
在小型机器(比如 2-4 个处理器)上,主内存可能是均匀的——与所有处理器的距离相等。在较大的机器(甚至在一些非常在小型机器中,内存可能不均匀——每个库可能在物理上与特定处理器或小组处理器相邻。然后,任何处理器中的核心都可以访问任何其他处理器的内存,但本地内存速度更快。当然,假设所有内存都被缓存,差异只会出现在缓存未命中上,此时本地内存的惩罚较低。
On small machines (2–4 processors, say), main memory may be uniform— equally distant from all processors. On larger machines (and even on some very small machines), memory may be nonuniform instead—each bank may be physically adjacent to a particular processor or small group of processors. Cores in any processor can then access the memory of any other, but local memory is faster. Assuming all memory is cached, of course, the difference appears only on cache misses, where the penalty for local memory is lower.
基于总线的缓存一致性算法现在是大多数商用微处理器的标准内置部分。在大型机器上,由于缺少广播总线,缓存一致性成为一个更加困难的问题;虽然有商用实现,但它们既复杂又昂贵。无论是在小型机器还是大型机器上,一致性都不是即时的(通知传播需要时间),这意味着我们必须从不同的处理器的角度考虑对不同位置的更新发生的顺序。确保一致性是一个令人惊讶的难题;我们将在第 13.3.3 节中再次讨论它。
Bus-based cache coherence algorithms are now a standard, built-in part of most commercial microprocessors. On large machines, the lack of a broadcast bus makes cache coherence a significantly more difficult problem; commercial implementations are available, but they are complex and expensive. On both small and large machines, the fact that coherence is not instantaneous (it takes time for notifications to propagate) means that we must consider the order in which updates to different locations appear to occur from the point of view of different processors. Ensuring a consistent view is a surprisingly difficult problem; we will return to it in Section 13.3.3.
截至 2015 年,每个主要指令集架构都有多核版本,包括 ARM、x86、Power、SPARC、x86-64 和 IA-64 (Itanium)。数十家制造商提供基于这些架构构建的小型缓存一致性多处理器。多家制造商提供更大型的缓存一致性共享内存多处理器,包括 Oracle、HP、IBM 和 SGI。
As of 2015, there are multicore versions of every major instruction set architecture, including ARM, x86, Power, SPARC, x86-64, and IA-64 (Itanium). Small, cache-coherent multiprocessors built from these are available from dozens of manufacturers. Larger, cache-coherent shared-memory multiprocessors are available from several manufacturers, including Oracle, HP, IBM, and SGI.
尽管与计算机行业的其他领域相比,超级计算在财务上相形见绌,但它在计算机发展中一直发挥着不成比例的作用技术和人类知识的进步。超级计算机随着时间的推移发生了巨大变化,并且继续以极快的速度发展。然而,它们一直都是并行机器。
Though dwarfed financially by the rest of the computer industry, supercomputing has always played a disproportionate role in the development of computer technology and the advancement of human knowledge. Supercomputers have changed dramatically over time, and they continue to evolve at a very rapid pace. They have always, however, been parallel machines.
由于缓存一致性的复杂性,很难构建大型共享内存机器。SGI 销售的机器最多有 256 个处理器(2048 个内核)。Cray 制造的共享内存机器甚至更大,但没有缓存远程位置的能力。然而,在大多数情况下,20 世纪 60 年代至 80 年代的矢量超级计算机并没有被大型多处理器取代,而是被少量的小型多处理器或大量商用(主流)处理器取代,这些处理器通过定制的高性能网络连接。随着网络技术“渗透”到更广泛的市场,这些机器又让位于由商用多核处理器和商用网络(千兆以太网或 Infiniband)组成的集群。截至 2015 年,集群已开始主宰从中等服务器场到除最快超级计算机站点之外的所有领域。谷歌、亚马逊或 Facebook 等大型在线服务通常由拥有数万或数十万个核心的集群支持(就谷歌而言,可能有数百万个)。
Because of the complexity of cache coherence, it is difficult to build large shared-memory machines. SGI sells machines with as many as 256 processors (2048 cores). Cray builds even larger shared-memory machines, but without the ability to cache remote locations. For the most part, however, the vector supercomputers of the 1960s–80s were displaced not by large multiprocessors, but by modest numbers of smaller multiprocessors or by very large numbers of commodity (mainstream) processors, connected by custom high-performance networks. As network technology “trickled down” into the broader market, these machines in turn gave way to clusters composed of both commodity multicore processors and commodity networks (Gigabit Ethernet or Infiniband). As of 2015, clusters have come to dominate everything from modest server farms up to all but the very fastest supercomputer sites. Large-scale on-line services like Google, Amazon, or Facebook are typically backed by clusters with tens or hundreds of thousands of cores (in Google's case, probably millions).
当今最快的计算机由特殊的高密度多核芯片构成,每核运行功耗较低。天河二号(截至 2015 年 6 月,世界上最快的计算机)采用 2:3 混合的英特尔 12 核 Ivy Bridge 和 61 核 Phi 处理器,每核功耗分别为 10 W 和 5 W。鉴于目前的趋势,未来的计算机(无论是高端计算机还是商用计算机)似乎都将越来越密集,越来越异构。
Today's fastest machines are constructed from special high-density multicore chips with low per-core operating power. The Tianhe-2 (the fastest machine in the world as of June 2015) uses a 2:3 mix of Intel 12-core Ivy Bridge and 61-core Phi processors, at 10 W and 5 W per core, respectively. Given current trends, it seems likely that future machines, both high-end and commodity, will be increasingly dense and increasingly heterogeneous.
从编程语言的角度来看,超级计算的特殊挑战在于适应不一致的访问时间,以及(在大多数情况下)整个机器缺乏对共享内存的硬件支持。当今的超级计算机大多使用消息传递库(尤其是 MPI)以及在本地和远程内存访问之间有明显语法区别的语言和库进行编程。
From a programming language perspective, the special challenge of supercomputing is to accommodate nonuniform access times and (in most cases) the lack of hardware support for shared memory across the full machine. Today's supercomputers are programmed mostly with message-passing libraries (MPI in particular) and with languages and libraries in which there is a clear syntactic distinction between local and remote memory access.
在并发程序中,我们将使用术语“线程”来指代程序员认为与其他线程并发运行的活动实体。在大多数系统中,给定程序的线程是在操作系统提供的一个或多个进程之上实现的。操作系统设计人员通常会区分重量级进程(具有自己的地址空间)和轻量级进程集合(可能共享一个地址空间)。在 20 世纪 80 年代末和 90 年代初,大多数 Unix 变体都添加了轻量级进程,以适应共享内存多处理器的普及。
Within a concurrent program, we will use the term thread to refer to the active entity that the programmer thinks of as running concurrently with other threads. In most systems, the threads of a given program are implemented on top of one or more processes provided by the operating system. OS designers often distinguish between a heavyweight process, which has its own address space, and a collection of lightweight processes, which may share an address space. Lightweight processes were added to most variants of Unix in the late 1980s and early 1990s, to accommodate the proliferation of shared-memory multiprocessors.
我们有时会使用“任务”一词来指代必须由某个线程执行的明确定义的工作单元。在一个常见的编程习语中,一组线程共享一个公共的“任务包”——要完成的工作列表。每个线程都会反复从任务包中取出一个任务,执行该任务,然后再返回执行另一个任务。有时,一项任务的工作需要向任务包中添加新任务。
We will sometimes use the word task to refer to a well-defined unit of work that must be performed by some thread. In one common programming idiom, a collection of threads shares a common “bag of tasks”—a list of work to be done. Each thread repeatedly removes a task from the bag, performs it, and goes back for another. Sometimes the work of a task entails adding new tasks to the bag.
不幸的是,不同系统和作者的术语并不一致。有几种语言将线程称为进程。Ada 将它们称为任务。有几种操作系统将轻量级进程称为线程。OSF Unix 和 Mac OS X 源自 Mach OS,它将轻量级进程共享的地址空间称为任务。一些系统试图通过创造新词来避免歧义,例如“actors”、“fibers”或“filaments”。我们将尝试一致地使用前两段的定义,并找出特定语言或系统的术语与此用法不同的情况。
Unfortunately, terminology is inconsistent across systems and authors. Several languages call their threads processes. Ada calls them tasks. Several operating systems call lightweight processes threads. The Mach OS, from which OSF Unix and Mac OS X are derived, calls the address space shared by lightweight processes a task. A few systems try to avoid ambiguity by coining new words, such as “actors,” “fibers,” or “filaments.” We will attempt to use the definitions of the preceding two paragraphs consistently, and to identify cases in which the terminology of particular languages or systems differs from this usage.
在任何并发编程模型中,要解决的两个最关键问题是通信和同步。通信是指允许一个线程获取另一个线程生成的信息的任何机制。命令式程序的通信机制通常基于共享内存或消息传递。在共享内存编程模型中,程序的部分或全部变量可供多个线程访问。对于要进行通信的两个线程,其中一个线程将值写入变量,而另一个线程则只是读取该值。在纯消息传递编程模型中,线程没有共同的状态:对于要进行通信的两个线程,其中一个线程必须执行显式发送操作才能将数据传输给另一个线程。(某些语言(例如 Ada、Go 和 Rust)同时提供消息和共享内存。)
In any concurrent programming model, two of the most crucial issues to be addressed are communication and synchronization. Communication refers to any mechanism that allows one thread to obtain information produced by another. Communication mechanisms for imperative programs are generally based on either shared memory or message passing. In a shared-memory programming model, some or all of a program's variables are accessible to multiple threads. For a pair of threads to communicate, one of them writes a value to a variable and the other simply reads it. In a pure message-passing programming model, threads have no common state: for a pair of threads to communicate, one of them must perform an explicit send operation to transmit data to another. (Some languages—Ada, Go, and Rust, for example—provide both messages and shared memory.)
同步是指允许程序员控制不同线程中操作发生的相对顺序的任何机制。同步在消息传递模型中通常是隐式的:必须先发送消息,然后才能接收。如果线程尝试接收尚未发送的消息,它将等待发送者赶上。同步在共享内存模型中通常不是隐式的:除非我们做一些特殊的事情,否则“接收”线程可能会在“发送者”写入变量之前读取该变量的“旧”值。
Synchronization refers to any mechanism that allows the programmer to control the relative order in which operations occur in different threads. Synchronization is generally implicit in message-passing models: a message must be sent before it can be received. If a thread attempts to receive a message that has not yet been sent, it will wait for the sender to catch up. Synchronization is generally not implicit in shared-memory models: unless we do something special, a “receiving” thread could read the “old” value of a variable, before it has been written by the “sender.”
在共享内存和基于消息的程序中,同步都可以通过旋转(也称为忙等待)或阻塞来实现。在忙等待同步中,线程运行一个循环,在该循环中,它不断重新评估某些条件,直到该条件变为真(例如,直到消息队列变为非空或共享变量达到特定值)——可能是由于在其他核心上运行的其他线程中的操作而导致的。请注意,忙等待在单处理器上毫无意义:我们不能指望在独占使条件成立所需的资源(唯一的核心)时条件会变为真。(单处理器上的线程有时可能会忙于等待 I/O 完成,但那是另一种情况:I/O 设备与处理器并行运行。)
In both shared-memory and message-based programs, synchronization can be implemented either by spinning (also called busy-waiting) or by blocking. In busy-wait synchronization, a thread runs a loop in which it keeps reevaluating some condition until that condition becomes true (e.g., until a message queue becomes nonempty or a shared variable attains a particular value)—presumably as a result of action in some other thread, running on some other core. Note that busy-waiting makes no sense on a uniprocessor: we cannot expect a condition to become true while we are monopolizing a resource (the one and only core) required to make it true. (A thread on a uniprocessor may sometimes busy-wait for the completion of I/O, but that's a different situation: the I/O device runs in parallel with the processor.)
在阻塞同步(也称为基于调度程序的同步)中,等待线程自愿将其核心让给其他线程。在这样做之前,它会在与同步条件关联的某个数据结构中留下一条注释。在未来某个时间点使条件成立的线程将找到该注释并采取行动使阻塞线程再次运行。我们将在13.2.4 节中再次简要讨论同步,然后在13.3 节中更详细地讨论同步。
In blocking synchronization (also called scheduler-based synchronization), the waiting thread voluntarily relinquishes its core to some other thread. Before doing so, it leaves a note in some data structure associated with the synchronization condition. A thread that makes the condition true at some point in the future will find the note and take action to make the blocked thread run again. We will consider synchronization again briefly in Section 13.2.4, and then more thoroughly in Section 13.3.
线程级并发性可以以显式并发语言、编译器支持的传统顺序语言扩展或语言本身之外的库包的形式提供给程序员。这三种选项都被广泛使用,尽管共享内存语言在“低端”(用于多核和小型多处理器机器)更常见,而消息传递库在“高端”(用于大规模并行超级计算机)更常见。图 13.4分类了广泛使用的系统示例。
Thread-level concurrency can be provided to the programmer in the form of explicitly concurrent languages, compiler-supported extensions to traditional sequential languages, or library packages outside the language proper. All three options are widely used, though shared-memory languages are more common at the “low end” (for multicore and small multiprocessor machines), and message-passing libraries are more common at the “high end” (for massively parallel supercomputers). Examples of systems in widespread use are categorized in Figure 13.4.
多年来,几乎所有的并行编程都采用传统的顺序语言(主要是 C 和 Fortran),并增加了用于同步或消息传递的库,这种方法至今仍占主导地位。在 Unix 世界中,共享内存并行性已基本融合到 POSIX pthreads标准中,该标准包括创建、销毁、调度和同步线程的机制。自 2011 年版以来,该标准已成为 C 和 C++ 的正式组成部分。Microsoft 的线程包和编译器为 Windows 计算机提供了类似的功能。对于高端科学计算,基于消息的并行性同样融合到 MPI(消息传递接口)标准中,几乎每个平台都有开源和商业实现。
For many years, almost all parallel programming employed traditional sequential languages (largely C and Fortran) augmented with libraries for synchronization or message passing, and this approach still dominates today. In the Unix world, shared memory parallelism has largely converged on the POSIX pthreads standard, which includes mechanisms to create, destroy, schedule, and synchronize threads. This standard became an official part of both C and C++ as of their 2011 versions. Similar functionality for Windows machines is provided by Microsoft's thread package and compilers. For high-end scientific computing, message-based parallelism has likewise converged on the MPI (Message Passing Interface) standard, with open-source and commercial implementations available for almost every platform.
虽然语言对并发的支持可以追溯到 Algol 68(而协程可以追溯到 Simula),并且这种支持在 20 世纪 80 年代末在 Ada 中得到了广泛应用,但直到 20 世纪 90 年代中期,人们才真正开始对这些功能产生广泛的兴趣,当时万维网的爆炸式增长开始推动并行服务器和并发客户端程序的发展。这一发展恰好与 Java 的推出相吻合,几年后微软又推出了 C#。尽管影响力还不如 C#,但许多其他语言,包括 Erlang、Go、Haskell、Rust 和 Scala,也明确支持并行。
While language support for concurrency goes back all the way to Algol 68 (and coroutines to Simula), and while such support was widely available in Ada by the late 1980s, widespread interest in these features didn't really arise until the mid-1990s, when the explosive growth of the World Wide Web began to drive the development of parallel servers and concurrent client programs. This development coincided nicely with the introduction of Java, and Microsoft followed with C# a few years later. Though not yet as influential, many other languages, including Erlang, Go, Haskell, Rust, and Scala, are explicitly parallel as well.
在科学编程领域,Fortran 的扩展历史悠久,旨在促进循环迭代的并行执行。到本世纪初,这项工作已基本集中在一组称为 OpenMP 的扩展上,不仅在 Fortran 中可用,而且在 C 和 C++ 中也可用。从语法上讲,OpenMP 包含一组编译指令(编译器指令),用于创建和同步线程,并在线程之间安排工作。在由多处理器网络组成的机器上,越来越常见的是看到在多处理器中使用 OpenMP 并在多处理器之间使用 MPI 的混合程序。
In the realm of scientific programming, there is a long history of extensions to Fortran designed to facilitate the parallel execution of loop iterations. By the turn of the century this work had largely converged on a set of extensions known as OpenMP, available not only in Fortran but also in C and C++. Syntactically, OpenMP comprises a set of pragmas (compiler directives) to create and synchronize threads, and to schedule work among them. On machines composed of a network of multiprocessors, it is increasingly common to see hybrid programs that use OpenMP within a multiprocessor and MPI across them.
在图 13.4的共享内存和消息传递列中,并行构造旨在用于单个多线程程序中。对于分布式系统中跨程序边界的通信,程序员传统上采用标准 Internet 协议的库实现,其方式让人联想到基于文件的 I/O(第 C-8.7 节)。然而,对于客户端-服务器交互,提供基于远程过程调用(RPC)的更高级接口可能很有吸引力,我们将在 C-13.5.4 节中进一步讨论这种替代方案。
In both the shared memory and message passing columns of Figure 13.4, the parallel constructs are intended for use within a single multithreaded program. For communication across program boundaries in distributed systems, programmers have traditionally employed library implementations of the standard Internet protocols, in a manner reminiscent of file-based I/O (Section C-8.7). For client-server interaction, however, it can be attractive to provide a higher-level interface based on remote procedure calls (RPC), an alternative we consider further in Section C-13.5.4.
与库包相比,显式并发编程语言具有编译器支持的优势。它可以使用除子例程调用之外的其他语法,并且可以将通信和线程管理与类型检查、作用域和异常等概念更紧密地集成在一起。同时,由于大多数程序历来都是顺序的,因此并发语言很难获得广泛接受,特别是考虑到并发功能的存在有时会使顺序情况更难理解。
In comparison to library packages, an explicitly concurrent programming language has the advantage of compiler support. It can make use of syntax other than subroutine calls, and can integrate communication and thread management more tightly with such concepts as type checking, scoping, and exceptions. At the same time, since most programs have historically been sequential, concurrent languages have been slow to gain widespread acceptance, particularly given that the presence of concurrent features can sometime make the sequential case more difficult to understand.
几乎每个并发系统都允许动态创建(和销毁)线程。不同语言或库的语法和语义细节差别很大,但大多数都符合六个主要选项之一:co-begin、并行循环、launch-at-elaboration、fork(带有可选的join)、隐式接收和早期回复。前两个选项使用特殊的控制流构造来界定线程。其他选项使用类似于(或等同于)子例程的语法。
Almost every concurrent system allows threads to be created (and destroyed) dynamically. Syntactic and semantic details vary considerably from one language or library to another, but most conform to one of six principal options: co-begin, parallel loops, launch-at-elaboration, fork (with optional join), implicit receipt, and early reply. The first two options delimit threads with special control-flow constructs. The others use syntax resembling (or identical to) subroutines.
至少有一种教学语言 (SR) 提供了所有六个选项。大多数其他语言则选择其中一种。大多数库使用fork/join,Java 和 C# 也是如此。Ada 同时使用 launch-at-elaboration 和fork。OpenMP 使用co-begin和 parallel 循环。RPC 系统通常基于隐式接收。
At least one pedagogical language (SR) provided all six options. Most others pick and choose. Most libraries use fork/join, as do Java and C#. Ada uses both launch-at-elaboration and fork. OpenMP uses co-begin and parallel loops. RPC systems are typically based on implicit receipt.
在许多系统中,程序员有责任确保循环迭代的并发执行是安全的,即正确性永远不会取决于竞争条件的结果。例如,对全局变量的访问通常必须同步,以确保迭代不会相互冲突。在一些语言(例如 Occam)中,语言规则禁止冲突访问。编译器会检查以确保一个线程写入的变量不会被任何并发活动线程读取或写入。类似但稍微灵活一些的是, Fortran 2008 的do parallel循环构成了程序员方面的断言,即循环的迭代是相互独立的,因此可以安全地按任何顺序或并行执行。循环内容的几条规则(其中一些但不是全部由编译器强制执行)降低了程序员错误地做出此断言的可能性。
In many systems it is the programmer's responsibility to make sure that concurrent execution of the loop iterations is safe, in the sense that correctness will never depend on the outcome of race conditions. Access to global variables, for example, must generally be synchronized, to make sure that iterations do not conflict with one another. In a few languages (e.g., Occam), language rules prohibit conflicting accesses. The compiler checks to make sure that a variable written by one thread is neither read nor written by any concurrently active thread. In a similar but slightly more flexible vein, the do concurrent loop of Fortran 2008 constitutes an assertion on the programmer's part that iterations of the loop are mutually independent, and hence can safely be executed in any order, or in parallel. Several rules on the content of the loop—some but not all of them enforceable by the compiler—reduce the likelihood that programmers will make this assertion incorrectly.
对于遍历数组元素的循环,forall语义非常适合在向量机上执行。对于更传统的多处理器,HPF 提供了一组广泛的数据分布和对齐指令,允许程序员将元素分散到与大量处理器相关的内存中。在forall循环中,给定赋值语句中的计算通常由“拥有”赋值左侧元素的处理器执行。在许多情况下,HPF 或 Fortran 95 编译器可以证明forall循环的某些(部分)组成语句之间没有依赖关系,并且可以允许它们继续进行而无需实际实现同步。
For loops that iterate over the elements of an array, the forall semantics are ideally suited for execution on a vector machine. For more conventional multiprocessors, HPF provides an extensive set of data distribution and alignment directives that allow the programmer to scatter elements across the memory associated with a large number of processors. Within a forall loop, the computation in a given assignment statement is usually performed by the processor that “owns” the element on the assignment's left-hand side. In many cases an HPF or Fortran 95 compiler can prove that there are no dependences among certain (portions of) constituent statements of a forall loop, and can allow them to proceed without actually implementing synchronization.
到目前为止,我们在所有示例中都假设新创建的线程将在创建者的地址空间中运行。在 RPC 系统中,通常希望自动创建新线程以响应来自其他地址空间的传入请求。服务器可以将通信通道绑定到本地线程体或子例程,而不是让现有线程执行接收操作。当请求传入时,将出现一个新线程来处理它。
We have assumed in all our examples so far that newly created threads will run in the address space of the creator. In RPC systems it is often desirable to create a new thread automatically in response to an incoming request from some other address space. Rather than have an existing thread execute a receive operation, a server can bind a communication channel to a local thread body or subroutine. When a request comes in, a new thread springs into existence to handle it.
实际上,绑定操作授予远程客户端在服务器地址空间内执行分叉的能力,尽管该过程通常不是完全自动化的。我们将在 C-13.5.4 节中更详细地讨论 RPC。
In effect, the bind operation grants remote clients the ability to perform a fork within the server's address space, though the process is often less than fully automatic. We will consider RPC in more detail in Section C-13.5.4.
然而,没有规定被调用者必须终止才能释放调用者;它真正要做的就是完成其工作的一部分,结果参数依赖。从Simula 中用于启动协程的分离操作(示例 9.47)中汲取灵感,一些语言(其中包括SR 和 Hermes [ SBG + 91 ])允许被调用方执行回复操作,该操作会在不终止的情况下将结果返回给调用方。在早期回复后,两个线程将同时继续。
Nothing dictates, however, that the callee has to terminate in order to release the caller; all it really has to do is complete the portion of its work on which result parameters depend. Drawing inspiration from the detach operation used to launch coroutines in Simula (Example 9.47), a few languages (SR and Hermes [SBG+91] among them) allow a callee to execute a reply operation that returns results to the caller without terminating. After an early reply, the two threads continue concurrently.
从语义上讲,被调用者在回复之前的部分与 Java 或 C# 线程的构造函数的作用非常相似;回复之后的部分则充当run方法的作用。通常的实现也类似,并且可能与程序员的直觉相反:由于早期回复是可选的,并且可以出现在任何子例程中,因此我们可以使用调用者的线程来执行被调用者的初始部分,并且仅当(并且如果)被调用者回复而不是返回时才创建新线程。
Semantically, the portion of the callee prior to the reply plays much the same role as the constructor of a Java or C# thread; the portion after the reply plays the role of the run method. The usual implementation is also similar, and may run counter to the programmer's intuition: since early reply is optional, and can appear in any subroutine, we can use the caller's thread to execute the initial portion of the callee, and create a new thread only when—and if—the callee replies instead of returning.
将每个线程放在单独的进程中的问题在于,进程(即使是“轻量级”进程)在许多操作系统中都过于昂贵。由于它们是在内核中实现的,因此对它们执行任何操作都需要系统调用。由于它们是通用的,因此它们提供了大多数语言不需要但无论如何都必须付费的功能。(示例包括单独的地址空间、优先级、记帐信息以及信号和 I/O 接口,所有这些都超出了本书的范围。)在另一个极端,将所有线程放在单个进程之上有两个问题:首先,它妨碍了在多核或多处理器计算机上并行执行;其次,如果当前正在运行的线程发出阻塞的系统调用(例如,等待 I/O),则程序的其他线程都无法运行,因为单个进程被操作系统暂停。
The problem with putting every thread on a separate process is that processes (even “lightweight” ones) are simply too expensive in many operating systems. Because they are implemented in the kernel, performing any operation on them requires a system call. Because they are general purpose, they provide features that most languages do not need, but have to pay for anyway. (Examples include separate address spaces, priorities, accounting information, and signal and I/O interfaces, all of which are beyond the scope of this book.) At the other extreme, there are two problems with putting all threads on top of a single process: first, it precludes parallel execution on a multicore or multiprocessor machine; second, if the currently running thread makes a system call that blocks (e.g., waiting for I/O), then none of the program's other threads can run, because the single process is suspended by the OS.
在常见的两级并发组织(内核级进程之上的用户级线程)中,系统的两个级别上都出现了类似的代码:语言运行时系统在一个或多个进程之上实现线程,其方式与操作系统在一个或多个物理核心之上实现进程的方式非常相似。在本节的其余部分,我们将使用进程之上的线程这一术语。
In the common two-level organization of concurrency (user-level threads on top of kernel-level processes), similar code appears at both levels of the system: the language run-time system implements threads on top of one or more processes in much the same way that the operating system implements processes on top of one or more physical cores. We will use the terminology of threads on top of processes in the remainder of this section.
典型的实现从协程开始(第 9.5 节)。回想一下,协程是一种顺序控制流机制:程序员可以通过调用transfer操作来暂停当前协程并恢复特定替代方案。transfer的参数通常是指向协程上下文块的指针。
The typical implementation starts with coroutines (Section 9.5). Recall that coroutines are a sequential control-flow mechanism: the programmer can suspend the current coroutine and resume a specific alternative by calling the transfer operation. The argument to transfer is typically a pointer to the context block of the coroutine.
要将协程转换为线程,我们需要执行三个步骤。首先,我们通过实现一个调度程序来隐藏 transfer 的参数,该调度程序会在当前线程让出核心时选择接下来要运行的线程。其次,我们实现一个抢占机制,该机制会定期自动暂停当前线程,让其他线程有机会运行。第三,我们允许多个 OS 进程(可能位于不同的核心上)共享描述线程集合的数据结构,以便线程可以在任何进程上运行。
To turn coroutines into threads, we proceed in a series of three steps. First, we hide the argument to transfer by implementing a scheduler that chooses which thread to run next when the current thread yields the core. Second, we implement a preemption mechanism that suspends the current thread automatically on a regular basis, giving other threads a chance to run. Third, we allow the data structures that describe our collection of threads to be shared by more than one OS process, possibly on separate cores, so that threads can run on any of the processes.
每当一个线程运行很长时间而其他线程处于可运行状态时,公平性就会成为一个问题。为了营造并发活动的假象,即使在单处理器上,我们也需要确保每个线程都能频繁地获得处理器的“切片”。对于协作式多线程,任何长时间运行的线程都必须不时地明确让出其核心(例如,在循环顶部),以允许其他线程运行。如第 13.1.1 节所述,这种方法允许一个编写不当的线程独占系统。即使使用正确编写的线程,由于不同线程之间让出时间不一致,也会导致不完美的公平性。
Fairness becomes an issue whenever a thread may run for a significant amount of time while other threads are runnable. To give the illusion of concurrent activity, even on a uniprocessor, we need to make sure that each thread gets a frequent “slice” of the processor. With cooperative multithreading, any long-running thread must yield its core explicitly from time to time (e.g., at the tops of loops), to allow other threads to run. As noted in Section 13.1.1, this approach allows one improperly written thread to monopolize the system. Even with properly written threads, it leads to less than perfect fairness due to nonuniform times between yields in different threads.
理想情况下,我们希望公平地、相对精细地(即每秒多次)复用每个核心,而无需线程显式调用yield。在许多系统上,我们可以在语言实现中通过使用定时器信号进行抢占式多线程来实现这一点。在线程之间切换时,我们要求操作系统(可以访问硬件时钟)在未来的指定时间向当前正在运行的进程发送信号。操作系统通过保存进程的上下文(寄存器和pc)并将控制权转移到语言运行时系统中先前指定的处理程序例程来传递信号,如第9.6.1节所述。调用时,处理程序会修改当前正在运行的线程的状态,使其看起来好像线程刚刚执行了对标准yield例程的调用,并且即将执行其序言。然后,处理程序“返回”到yield,将控制权转移到其他线程,就好像正在运行的线程自愿放弃了对进程的控制一样。
Ideally, we should like to multiplex each core fairly and at a relatively fine grain (i.e., many times per second) without requiring that threads call yield explicitly. On many systems we can do this in the language implementation by using timer signals for preemptive multithreading. When switching between threads we ask the operating system (which has access to the hardware clock) to deliver a signal to the currently running process at a specified time in the future. The OS delivers the signal by saving the context (registers and pc) of the process and transferring control to a previously specified handler routine in the language run-time system, as described in Section 9.6.1. When called, the handler modifies the state of the currently running thread to make it appear that the thread had just executed a call to the standard yield routine, and was about to execute its prologue. The handler then “returns” into yield, which transfers control to some other thread, as if the one that had been running had relinquished control of the process voluntarily.
我们可以扩展抢占式线程包,使其在多个 OS 提供的进程上运行,方法是安排进程共享就绪列表和相关数据结构(条件队列等;请注意,每个进程必须具有单独的 current_thread变量)。如果进程在不同的物理核心上运行,则将能够同时运行多个线程。如果进程共享单个核心,那么即使操作系统中除一个进程外所有进程都被阻止,程序也将能够向前推进。任何可运行的线程都放置在就绪列表中,它将成为应用程序任何进程的执行候选。当进程调用reschedule时,我们在示例中使用的基于队列的就绪列表将为其提供等待时间最长的线程。更复杂的调度程序的就绪列表可能会优先考虑交互式或时间关键型线程,或者优先考虑上次在当前核心上运行的线程,因此可能仍在缓存中保留数据。
We can extend our preemptive thread package to run on top of more than one OS-provided process by arranging for the processes to share the ready list and related data structures (condition queues, etc.; note that each process must have a separate current_thread variable). If the processes run on different physical cores, then more than one thread will be able to run at once. If the processes share a single core, then the program will be able to make forward progress even when all but one of the processes are blocked in the operating system. Any thread that is runnable is placed in the ready list, where it becomes a candidate for execution by any of the application's processes. When a process calls reschedule, the queue-based ready list we have been using in our examples will give it the longest-waiting thread. The ready list of a more elaborate scheduler might give priority to interactive or time-critical threads, or to threads that last ran on the current core, and may therefore still have data in the cache.
正如抢占引入了对调度程序操作的自愿调用和自动调用之间的竞争一样,真正的或准并行性引入了不同 OS 进程中调用之间的竞争。为了解决竞争,我们必须实现额外的同步,以使不同进程中的调度程序操作具有原子性。我们将在第 13.3.4 节中回到这个主题。
Just as preemption introduced a race between voluntary and automatic calls to scheduler operations, true or quasiparallelism introduces races between calls in separate OS processes. To resolve the races, we must implement additional synchronization to make scheduler operations in separate processes atomic. We will return to this subject in Section 13.3.4.
如第 13.2.1 节所述,同步是共享内存并发程序的主要语义挑战。通常,同步用于使某些操作原子化或延迟该操作直到某些必要的先决条件成立。如第 13.1 节所述,原子性最常通过互斥锁实现。互斥确保在给定时间点只有一个线程正在执行某个关键代码段。关键代码段通常将共享数据结构从一种一致状态转换为另一种一致状态。
As noted in Section 13.2.1, synchronization is the principal semantic challenge for shared-memory concurrent programs. Typically, synchronization serves either to make some operation atomic or to delay that operation until some necessary precondition holds. As noted in Section 13.1, atomicity is most commonly achieved with mutual exclusion locks. Mutual exclusion ensures that only one thread is executing some critical section of code at a given point in time. Critical sections typically transform a shared data structure from one consistent state to another.
条件同步允许线程等待先决条件,通常表示为一个或多个共享变量中的值的谓词。人们很容易将互斥视为条件同步的一种形式(直到没有其他线程处于其临界区时才继续),但这种条件需要所有现有线程之间达成共识,而条件同步通常不提供这一点。
Condition synchronization allows a thread to wait for a precondition, often expressed as a predicate on the value(s) in one or more shared variables. It is tempting to think of mutual exclusion as a form of condition synchronization (don't proceed until no other thread is in its critical section), but this sort of condition would require consensus among all extant threads, something that condition synchronization doesn't generally provide.
我们的并行线程实现(在13.2.4 节末尾进行了概述)需要原子性和条件同步。就绪列表和相关数据结构上的操作的原子性确保它们始终满足一组逻辑不变量:列表格式正确,每个线程要么正在运行,要么恰好驻留在一个列表中,等等。条件同步出现在以下要求中:需要运行线程的进程必须等到就绪列表非空。
Our implementation of parallel threads, sketched at the end of Section 13.2.4, requires both atomicity and condition synchronization. Atomicity of operations on the ready list and related data structures ensures that they always satisfy a set of logical invariants: the lists are well formed, each thread is either running or resides in exactly one list, and so forth. Condition synchronization appears in the requirement that a process in need of a thread to run must wait until the ready list is nonempty.
值得强调的是,我们通常不希望过度同步程序。这样做会消除并行的机会,而为了提高性能,我们通常希望最大限度地提高并行性。此外,并非所有竞争都是坏的。如果两个进程正在竞争将最后一个线程从就绪列表中出队,我们通常不关心哪个成功,哪个等待另一个线程。我们关心的是出队的实现没有内部的指令级竞争,这可能会损害就绪列表的完整性。一般来说,我们的目标是仅提供必要的同步以消除“坏”竞争 - 否则可能会导致程序产生不正确的结果。
It is worth emphasizing that we do not in general want to overly synchronize programs. To do so would eliminate opportunities for parallelism, which we generally want to maximize in the interest of performance. Moreover not all races are bad. If two processes are racing to dequeue the last thread from the ready list, we don't generally care which succeeds and which waits for another thread. We do care that the implementation of dequeue does not have internal, instruction-level races that might compromise the ready list's integrity. In general, our goal is to provide only as much synchronization as is necessary to eliminate “bad” races— those that might otherwise cause the program to produce incorrect results.
在下面的第一小节中,我们考虑忙等待同步。在第二小节中,我们提出了一种称为非阻塞同步的替代方案,其中无需互斥即可实现原子性。在第三小节中,我们回到内存一致性的主题(最初在13.1.2 节中提到),并讨论其对语言级同步机制的语义和实现的影响。最后,在13.3.4和13.3.5节中,我们使用进程间的忙等待来实现并行安全的线程调度程序,然后依次使用此调度程序来实现最基本的基于调度程序的同步机制:即信号量。
In the first subsection below we consider busy-wait synchronization. In the second we present an alternative, called nonblocking synchronization, in which atomicity is achieved without the need for mutual exclusion. In the third subsection we return to the subject of memory consistency (originally mentioned in Section 13.1.2), and discuss its implications for the semantics and implementation of language-level synchronization mechanisms. Finally, in Sections 13.3.4 and 13.3.5, we use busy-waiting among processes to implement a parallelism-safe thread scheduler, and then use this scheduler in turn to implement the most basic scheduler-based synchronization mechanism: namely, semaphores.
如果我们可以把条件转换为“位置X包含值Y ”的形式,那么忙等待条件同步就很容易了:需要等待条件的线程可以简单地循环读取X ,等待Y出现。要等待涉及多个位置的条件,需要原子性来同时读取位置,但考虑到这一点,实现又是一个简单的循环。
Busy-wait condition synchronization is easy if we can cast a condition in the form of “location X contains value Y“: a thread that needs to wait for the condition can simply read X in a loop, waiting for Y to appear. To wait for a condition involving more than one location, one needs atomicity to read the locations together, but given that, the implementation is again a simple loop.
其他形式的忙等待同步稍微复杂一些。在本节的剩余部分,我们将讨论自旋锁(提供互斥)和屏障(确保所有线程都到达该点之前,任何线程都不会继续超过程序中的给定点)。
Other forms of busy-wait synchronization are somewhat trickier. In the remainder of this section we consider spin locks, which provide mutual exclusion, and barriers, which ensure that no thread continues past a given point in a program until all threads have reached that point.
一般认为,Dekker 发现了第一个双线程互斥算法,该算法不需要除加载和存储之外的原子指令。Dijkstra [ Dij65 ] 于 1965 年发表了一个适用于n 个线程的版本。Peterson [ Pet81 ] 于 1981 年发表了一个简单得多的双线程算法。以 Peterson 算法为基础,可以构造一个分层的n线程锁,但是这需要O ( nlogn )空间和O (logn )时间才能让一个线程进入其临界区 [ YA93 ]。Lamport [ Lam87 ] 2于 1987 年发表了一个n线程算法,在没有锁竞争的情况下,该算法占用空间为O ( n ),时间是O (1)。不幸的是,当多个线程同时尝试进入其临界区时,该算法需要O ( n ) 时间。
Dekker is generally credited with finding the first two-thread mutual exclusion algorithm that requires no atomic instructions other than load and store. Dijkstra [Dij65] published a version that works for n threads in 1965. Peterson [Pet81] published a much simpler two-thread algorithm in 1981. Building on Peterson's algorithm, one can construct a hierarchical n-thread lock, but it requires O(n log n) space and O(log n) time to get one thread into its critical section [YA93]. Lamport [Lam87]2 published an n-thread algorithm in 1987 that takes O(n) space and O(1) time in the absence of competition for the lock. Unfortunately, it requires O(n) time when multiple threads attempt to enter their critical section at once.
实际上,在循环中嵌入test_and_set往往会导致多核或多处理器机器上出现不可接受的通信量,因为缓存一致性机制会尝试协调多个试图获取锁的内核的写入。这种对硬件资源的过度需求被称为争用,是大型机器上实现良好性能的主要障碍。
In practice, embedding test_and_set in a loop tends to result in unacceptable amounts of communication on a multicore or multiprocessor machine, as the cache coherence mechanism attempts to reconcile writes by multiple cores attempting to acquire the lock. This overdemand for hardware resources is known as contention, and is a major obstacle to good performance on large machines.
许多处理器提供比test_and_set更强大的原子指令。有些可以原子地交换寄存器和内存位置的内容。有些可以原子地将常量添加到内存位置,并返回先前的值。包括 x86、IA-64 和 SPARC 在内的多种处理器都提供了特别有用的指令,称为compare_and_swap (CAS)。此指令接受三个参数:位置、预期值和新值。它检查预期值是否出现在指定位置,如果是,则原子地用新值替换它。无论哪种情况,它都会返回是否进行了更改的指示。使用atomic_add或compare_and_swap之类的指令,可以构建公平的自旋锁,即保证线程按照它们第一次尝试的顺序获取锁。还可以构建在任意大型机器上工作良好的锁 — 即使在释放时也不会发生争用 [ MCS91、Sco13 ]。这些主题超出了本文的范围。(也许值得一提的是,公平是一把双刃剑:虽然从语义的角度来看公平可能是可取的,但它往往会破坏缓存局部性,并且与抢占的相互作用非常糟糕。)
Many processors provide atomic instructions more powerful than test_and_set. Some can swap the contents of a register and a memory location atomically. Some can add a constant to a memory location atomically, returning the previous value. Several processors, including the x86, the IA-64, and the SPARC, provide a particularly useful instruction called compare_and_swap (CAS). This instruction takes three arguments: a location, an expected value, and a new value. It checks to see whether the expected value appears in the specified location, and if so replaces it with the new value, atomically. In either case, it returns an indication of whether the change was made. Using instructions like atomic_add or compare_and_swap, one can build spin locks that are fair, in the sense that threads are guaranteed to acquire the lock in the order in which they first attempt to do so. One can also build locks that work well—with no contention, even at release time—on arbitrarily large machines [MCS91, Sco13]. These topics are beyond the scope of the current text. (It is perhaps worth mentioning that fairness is a two-edged sword: while it may be desirable from a semantic point of view, it tends to undermine cache locality, and interacts very badly with preemption.)
互斥锁的一个重要变体是读写锁[ CHP71 ]。读写锁可以识别如果多个线程希望读取同一个数据结构,它们可以同时读取而不会相互干扰。只有当一个线程想要写入数据结构时,我们才需要阻止其他线程同时读取或写入。大多数忙等待互斥锁都可以扩展为允许读者并发访问(参见练习 13.8)。
An important variant on mutual exclusion is the reader–writer lock [CHP71]. Reader–writer locks recognize that if several threads wish to read the same data structure, they can do so simultaneously without mutual interference. It is only when a thread wants to write the data structure that we need to prevent other threads from reading or writing simultaneously. Most busy-wait mutual exclusion locks can be extended to allow concurrent access by readers (see Exercise 13.8).
数据并行算法通常由一系列高级步骤或阶段构成,通常表示为某个最外层循环的迭代。正确性通常取决于确保每个线程在进入下一步之前完成上一步。屏障用于提供这种同步。
Data-parallel algorithms are often structured as a series of high-level steps, or phases, typically expressed as iterations of some outermost loop. Correctness often depends on making sure that every thread completes the previous step before any moves on to the next. A barrier serves to provide this synchronization.
和简单的自旋锁一样,“反向感知”屏障在大型机器上会导致不可接受的争用水平。此外,对计数器的访问串行化意味着实现n线程屏障的时间是O ( n )。可以做得更好,但即使是最快的软件屏障也需要O (log n ) 时间来同步n 个线程 [ MCS91 ]。大型多处理器有时会提供特殊硬件来将此界限缩短到接近常数时间。
Like a simple spin lock, the “sense-reversing” barrier can lead to unacceptable levels of contention on large machines. Moreover the serialization of access to the counter implies that the time to achieve an n-thread barrier is O(n). It is possible to do better, but even the fastest software barriers require O(log n) time to synchronize n threads [MCS91]. Large multiprocessors sometimes provide special hardware to reduce this bound to close to constant time.
到目前为止,我们在讨论中一直使用操作系统中“阻塞”的定义:阻塞的线程放弃核心而不是主动旋转。另一种定义来自并发算法理论。在这里,旋转和放弃核心之间的选择并不重要:如果线程在没有其他线程操作的情况下无法向前推进,则称该线程为“阻塞”。相反,如果在系统的每个可到达状态下,执行该操作的任何线程在自行运行(不受其他线程进一步干扰)的情况下都能保证在有限步骤内完成,则称该操作为非阻塞。
In our discussions thus far, we have used a definition of “blocking” that comes from operating systems: a thread that blocks gives up the core instead of actively spinning. An alternative definition comes from the theory of concurrent algorithms. Here the choice between spinning and giving up the core is immaterial: a thread is said to be “blocked” if it cannot make forward progress without action by other threads. Conversely, an operation is said to be nonblocking if in every reachable state of the system, any thread executing that operation is guaranteed to complete in a finite number of steps if it gets to run by itself (without further interference by other threads).
从这个理论意义上讲,无论实现方式如何,锁本质上都是阻塞的:如果一个线程持有锁,则其他需要该锁的线程都无法继续。相比之下,示例 13.29中基于CAS的代码是非阻塞的:如果CAS操作失败,那是因为其他线程已经取得了进展。此外,如果除了一个线程之外的所有线程都停止运行(例如由于抢占),则保证剩下的线程能够取得进展。
In this theoretical sense of the word, locks are inherently blocking, regardless of implementation: if one thread holds a lock, no other thread that needs that lock can proceed. By contrast, the CAS-based code of Example 13.29 is nonblocking: if the CAS operation fails, it is because some other thread has made progress. Moreover if all threads but one stop running (e.g., because of preemption), the remaining thread is guaranteed to make progress.
我们可以从示例 13.29中推广到设计各种无需锁即可操作的专用并发数据结构。对这些结构的修改通常(但并非总是)遵循以下模式
We can generalize from Example 13.29 to design a variety of special-purpose concurrent data structures that operate without locks. Modifications to these structures often (though not always) follow the pattern
准备
prepare
CAS——(或其他原子操作)
CAS –– (or some other atomic operation)
直至成功
until success
清理
clean up
如果读取多个位置,算法的“准备”部分可能需要进行双重检查,以确保没有任何值发生变化(即所有值均已一致读取),然后再转到 CAS。一旦此双重检查成功,只读操作可能只会返回。
If it reads more than one location, the “prepare” part of the algorithm may need to double-check to make sure that none of the values has changed (i.e., that all were read consistently) before moving on to the CAS. A read-only operation may simply return once this double-checking is successful.
在示例 13.29的基于CAS 的更新中,算法的“准备”部分读取 × 的旧值并确定新值应该是什么;“清理”部分为空。在其他算法中可能会有大量清理工作。在所有情况下,正确性的关键在于:(1)如果需要重复,“准备”部分是无害的;(2)如果CAS成功,则在逻辑上以所有其他线程可见的方式完成操作;(3)如果需要,“清理”可以由任何线程在原始线程延迟时执行。为另一个线程的操作执行清理通常称为帮助。
In the CAS-based update of Example 13.29, the “prepare” part of the algorithm reads the old value of × and figures out what the new value ought to be; the “clean up” part is empty. In other algorithms there may be significant cleanup. In all cases, the keys to correctness are that (1) the “prepare” part is harmless if we need to repeat; (2) the CAS, if successful, logically completes the operation in a way that is visible to all other threads; and (3) the “clean up,” if needed, can be performed by any thread if the original thread is delayed. Performing cleanup for another thread's operation is often referred to as helping.
非阻塞算法比阻塞算法有几个优点。它们天生就容忍页面错误和抢占:如果一个线程在操作中途停止运行,它绝不会阻止其他线程继续执行。非阻塞算法还可以安全地用于信号(事件)和中断处理程序,从而避免出现示例 13.22中描述的问题。对于几个重要的数据结构和算法(包括堆栈、队列、排序列表、优先级队列、哈希表和内存管理),非阻塞算法也可以比锁更快。不幸的是,这些算法往往非常微妙且难以设计。它们主要用于语言级并发机制的实现和标准库包中。
Nonblocking algorithms have several advantages over blocking algorithms. They are inherently tolerant of page faults and preemption: if a thread stops running partway through an operation, it never prevents other threads from making progress. Nonblocking algorithms can also safely be used in signal (event) and interrupt handlers, avoiding problems like the one described in Example 13.22. For several important data structures and algorithms, including stacks, queues, sorted lists, priority queues, hash tables, and memory management, nonblocking algorithms can also be faster than locks. Unfortunately, these algorithms tend to be exceptionally subtle and difficult to devise. They are used primarily in the implementation of language-level concurrency mechanisms and in standard library packages.
到目前为止,在我们所有的讨论中,我们都隐含地依赖于硬件内存一致性。不幸的是,单靠一致性不足以使多处理器甚至单个多核处理器按照大多数程序员的期望运行。当多个位置几乎同时被写入时,我们还必须担心写入对不同核心可见的顺序。
In all our discussions so far, we have depended, implicitly, on hardware memory coherence. Unfortunately, coherence alone is not enough to make a multiprocessor or even a single multicore processor behave as most programmers would expect. We must also worry, when more than one location is written at about the same time, about the order in which the writes become visible to different cores.
直观地讲,大多数程序员都希望共享内存具有顺序一致性— 即所有写入操作都以相同的顺序对所有内核可见,并且任何给定内核的写入操作都以执行顺序可见。不幸的是,这种行为很难有效实现 — 难到大多数硬件设计人员根本不提供它。相反,他们提供了几种宽松的内存模型之一,其中某些加载和存储可能看起来“无序”发生。宽松一致性对语言设计人员、编译器编写人员以及同步机制和非阻塞算法的实现者具有重要影响。
Intuitively, most programmers expect shared memory to be sequentially consistent—to make all writes visible to all cores in the same order, and to make any given core's writes visible in the order they were performed. Unfortunately, this behavior turns out to be very hard to implement efficiently—hard enough that most hardware designers simply don't provide it. Instead, they provide one of several relaxed memory models, in which certain loads and stores may appear to occur “out of order.” Relaxed consistency has important ramifications for language designers, compiler writers, and the implementors of synchronization mechanisms and nonblocking algorithms.
顺序一致性的基本问题在于,直接的实现需要硬件和编译器来序列化我们宁愿以任意顺序执行的操作。
The fundamental problem with sequential consistency is that straightforward implementations require both hardware and compilers to serialize operations that we would rather be able to perform in arbitrary order.
为了避免时间循环,并发语言和库的实现者通常必须使用特殊的同步或内存隔离指令。这些指令会以一定的代价强制执行硬件通常无法保证的排序。3它们的存在还会抑制编译器中的指令重新排序。
To avoid temporal loops, implementors of concurrent languages and libraries must generally use special synchronization or memory fence instructions. At some expense, these force orderings not normally guaranteed by the hardware.3 Their presence also inhibits instruction reordering in the compiler.
在示例 13.31中,A和B都必须防止其读取绕过(在之前完成)逻辑上更早的写入。通常,这可以通过将读取或写入标识为同步指令(例如,通过在x86 上使用XCHG指令实现)或在它们之间插入隔离(例如,在 SPARC 上插入membar StoreLoad )来实现。
In Example 13.31, both A and B must prevent their read from bypassing (completing before) the logically earlier write. Typically this can be accomplished by identifying either the read or the write as a synchronization instruction (e.g., by implementing it with an XCHG instruction on the x86) or by inserting a fence between them (e.g., membar StoreLoad on the SPARC).
对于在单核上运行的程序,无论底层管道和内存层次结构的复杂性如何,每个制造商都保证指令将按程序顺序出现:任何指令都会看到某些前一条指令的效果,也不会看到任何后续指令的效果。对于在多核或多处理器机器上运行的程序,制造商还保证在某些情况下,在一个核心上执行的指令将按顺序被其他核心上的指令看到。不幸的是,这些情况因机器而异。MIPS 和 PA-RISC 处理器的一些实现是顺序一致的,IBM 的 z 系列大型机也是如此:如果核心B上的负载看到写入的值通过在核心A上进行存储,那么,传递地,保证在核心A上存储之前的所有操作都发生在核心B上加载之后的所有操作之前。其他处理器和实现则更为宽松。特别是,大多数机器都允许示例 13.31中的循环。SPARC 和 x86 禁止示例 13.32中的循环(图 13.11),但 Power、ARM 和 Itanium 都允许它。
For programs running on a single core, regardless of the complexity of the underlying pipeline and memory hierarchy, every manufacturer guarantees that instructions will appear to occur in program order: no instruction will fail to see the effects of some prior instruction, nor will it see the effects of any subsequent instruction. For programs running on a muilticore or multiprocessor machine, manufacturers also guarantee, under certain circumstances, that instructions executed on one core will be seen, in order, by instructions on other cores. Unfortunately, these circumstances vary from one machine to another. Some implementations of the MIPS and PA-RISC processors were sequentially consistent, as are IBM's z-Series mainframe machines: if a load on core B sees a value written by a store on core A, then, transitively, everything before the store on A is guaranteed to have happened before everything after the load on B. Other processors and implementations are more relaxed. In particular, most machines admit the loop of Example 13.31. The SPARC and x86 preclude the loop of Example 13.32 (Figure 13.11), but the Power, ARM, and Itanium all allow it.
考虑到不同机器之间的差异,语言设计者该怎么做呢?答案是 Sarita Adve 首次提出的,现在已嵌入 Java、C++ 和(不太正式的)其他语言中,即定义一个内存模型来捕捉“正确同步”程序的概念,然后为所有此类程序提供顺序一致性的假象。实际上,内存模型构成了程序员和语言实现之间的契约:如果程序员遵循契约规则,实现将隐藏底层硬件的所有排序怪癖。
Given this variation across machines, what is a language designer to do? The answer, first suggested by Sarita Adve and now embedded in Java, C++, and (less formally) other languages, is to define a memory model that captures the notion of a “properly synchronized” program, and then provide the illusion of sequential consistency for all such programs. In effect, the memory model constitutes a contract between the programmer and the language implementation: if the programmer follows the rules of the contract, the implementation will hide all the ordering eccentricities of the underlying hardware.
在通常的表述中,内存模型区分“普通”变量访问和特殊同步访问;后者不仅包括锁获取和释放,还包括对用特殊同步关键字(Java 或 C# 中的volatile ,C++ 中的atomic)声明的任何变量的读写。跨核心操作的顺序完全基于同步访问。如果 (1)在单个线程中,A按程序顺序位于 C 之前;( 2) A和C是同步操作,并且语言定义表明A位于C之前;或 (3) 存在操作B使得A HB B和B HB C ,则我们说操作A 发生在操作 C ( A HB C )之前。
In the usual formulation, memory models distinguish between “ordinary” variable accesses and special synchronization accesses; the latter include not only lock acquire and release, but also reads and writes of any variable declared with a special synchronization keyword (volatile in Java or C#, atomic in C++). Ordering of operations across cores is based solely on synchronization accesses. We say that operation A happens before operation C (A HB C) if (1) A comes before C in program order in a single thread; (2) A and C are synchronization operations and the language definition says that A comes before C; or (3) there exists an operation B such that A HB B and B HB C.
如果两个普通访问发生在不同的线程中,它们引用相同的位置,并且其中至少有一个是写入,则称它们发生冲突。如果它们发生冲突并且没有排序,则称它们构成数据争用- 实现可能允许其中任何一个先发生,并且程序行为可能会因此而改变。根据这些定义,内存模型契约很简单:无数据争用程序的执行始终是顺序一致的。
Two ordinary accesses are said to conflict if they occur in different threads, they refer to the same location, and at least one of them is a write. They are said to constitute a data race if they conflict and they are not ordered—the implementation might allow either one to happen first, and program behavior might change as a result. Given these definitions, the memory model contract is straightforward: executions of data-race–free programs are always sequentially consistent.
在大多数情况下,互斥锁的获取顺序是在最近的释放之后。易失性(原子)变量的读取顺序是在存储读取值的写入之后。各种其他操作(例如,我们将在13.4.4 节中研究的事务)也可能有助于跨线程排序。
In most cases, an acquire of a mutual exclusion lock is ordered after the most recent prior release. A read of a volatile (atomic) variable is ordered after the write that stored the value that was read. Various other operations (e.g., the transactions we will study in Section 13.4.4) may also contribute to cross-thread ordering.
同步竞争在多线程程序中很常见。它们是错误还是预期行为取决于应用程序。另一方面,数据竞争几乎总是程序错误。它们很难推理 - 而且很少有用 - 因此 C++ 内存模型完全禁止它们:给定一个具有数据竞争的程序,C++ 实现可以显示任何行为。Ada有类似的规则。相比之下,对于 Java,对嵌入式应用程序的重视促使语言设计者以能够保持底层虚拟机完整性的方式限制竞争程序的行为。包含数据竞争的 Java 程序必须继续遵循所有正常的语言规则,并且任何相对于唯一的先前写入无序的读取都必须返回一个值,该值可能是由某个先前写入到同一位置的写入或相对于读取无序的写入写入的。在讨论语言的同步机制之后,我们将在第 13.4.3 节中返回 Java 内存模型。
Synchronization races are common in multithreaded programs. Whether they are bugs or expected behavior depends on the application. Data races, on the other hand, are almost always program bugs. They are so hard to reason about— and so rarely useful—that the C++ memory model outlaws them altogether: given a program with a data race, a C++ implementation is permitted to display any behavior whatsoever. Ada has similar rules. For Java, by contrast, an emphasis on embedded applications motivated the language designers to constrain the behavior of racy programs in ways that would preserve the integrity of the underlying virtual machine. A Java program that contains a data race must continue to follow all the normal language rules, and any read that is not ordered with respect to a unique preceding write must return a value that might have been written by some previous write to the same location, or by a write that is unordered with respect to the read. We will return to the Java Memory Model in Section 13.4.3, after we have discussed the language's synchronization mechanisms.
在大型多处理器上,我们可以通过为每个条件队列使用单独的锁,为就绪列表使用另一个锁来提高并发性。但是,我们必须小心,确保一个进程不可能将线程放入条件队列(或就绪列表),而另一个进程不可能在第一个进程完成从该线程的转移之前尝试转移到该线程(参见练习 13.13)。
On a large multiprocessor we might increase concurrency by employing a separate lock for each condition queue, and another for the ready list. We would have to be careful, however, to make sure it wasn't possible for one process to put a thread into a condition queue (or the ready list) and for another process to attempt to transfer into that thread before the first process had finished transferring out of it (see Exercise 13.13).
忙等待同步的问题在于它会消耗处理器周期,因此这些周期无法用于其他计算。忙等待同步只有在以下情况下才有意义:(1) 当前核心没有更好的事情可做,或 (2) 预期等待时间小于将上下文切换到其他线程然后再切换回来所需的时间。为了确保在各种系统上都能获得可接受的性能,大多数并发编程语言都采用基于调度程序的同步机制,当正在运行的线程阻塞时,这些机制会切换到其他线程。
The problem with busy-wait synchronization is that it consumes processor cycles—cycles that are therefore unavailable for other computation. Busy-wait synchronization makes sense only if (1) one has nothing better to do with the current core, or (2) the expected wait time is less than the time that would be required to switch contexts to some other thread and then switch back again. To ensure acceptable performance on a wide variety of systems, most concurrent programming languages employ scheduler-based synchronization mechanisms, which switch to a different thread when the one that was running blocks.
在下一小节中,我们将讨论信号量,这是最常见的基于调度程序的同步形式。在第 13.4 节中,我们将讨论监视器、条件临界区和事务内存等更高级别的概念。在每种情况下,基于调度程序的同步机制都会从调度程序的就绪列表中删除等待线程,只有当等待条件为真(或可能为真)时才返回该线程。相比之下,自旋然后让出锁仍然是一种忙等待机制:当前运行的进程放弃核心,但仍保留在就绪列表中。每次锁似乎空闲时,它都会执行test_and_set操作,直到最终成功。值得注意的是,忙等待同步通常是“级别独立的”——可以根据需要将其视为同步线程、进程或核心。基于调度程序的同步是“级别相关的”——当在语言运行时系统中实现时,它特定于线程,当在操作系统中实现时,它特定于进程。
In the following subsection we consider semaphores, the most common form of scheduler-based synchronization. In Section 13.4 we consider the higher-level notions of monitors, conditional critical regions, and transactional memory. In each case, scheduler-based synchronization mechanisms remove the waiting thread from the scheduler's ready list, returning it only when the awaited condition is true (or is likely to be true). By contrast, a spin-then-yield lock is still a busy-wait mechanism: the currently running process relinquishes the core, but remains on the ready list. It will perform a test_and_set operation every time the lock appears to be free, until it finally succeeds. It is worth noting that busy-wait synchronization is generally “level-independent”—it can be thought of as synchronizing threads, processes, or cores, as desired. Scheduler-based synchronization is “level-dependent”—it is specific to threads when implemented in the language run-time system, or to processes when implemented in the operating system.
信号量是最古老的基于调度器的同步机制。它们由 Dijkstra 在 20 世纪 60 年代中期描述 [ Dij68a ],并出现在 Algol 68 中。它们至今仍被广泛使用,特别是在基于库的并发实现中。
Semaphores are the oldest of the scheduler-based synchronization mechanisms. They were described by Dijkstra in the mid-1960s [Dij68a], and appear in Algol 68. They are still heavily used today, particularly in library-based implementations of concurrency.
计数器初始化为 1 且P和V操作总是成对出现的信号量称为二进制信号量。它用作基于调度程序的互斥锁:P操作获取锁;V释放锁。更一般地,计数器初始化为k的信号量可用于仲裁对某些资源的k 个副本的访问。计数器在任何特定时间的值表示当前未使用的副本数。练习 13.19指出二进制信号量可用于实现通用信号量,因此两者具有相同的表达能力,即使便利性不相等。
A semaphore whose counter is initialized to one and for which P and V operations always occur in matched pairs is known as a binary semaphore. It serves as a scheduler-based mutual exclusion lock: the P operation acquires the lock; V releases it. More generally, a semaphore whose counter is initialized to k can be used to arbitrate access to k copies of some resource. The value of the counter at any particular time indicates the number of copies not currently in use. Exercise 13.19 notes that binary semaphores can be used to implement general semaphores, so the two are of equal expressive power, if not of equal convenience.
尽管信号量被广泛使用,但人们普遍认为它对于结构良好、可维护的代码来说太“低级”了。它们有两个主要问题。首先,由于它们的操作只是子程序调用,因此很容易遗漏一个(例如,在具有多个嵌套if语句的控制路径上)。其次,除非它们隐藏在抽象中,否则给定信号量的使用往往会分散在整个程序中,这使得很难为了软件维护而追踪它们。
Though widely used, semaphores are also widely considered to be too “low level” for well-structured, maintainable code. They suffer from two principal problems. First, because their operations are simply subroutine calls, it is easy to leave one out (e.g., on a control path with several nested if statements). Second, unless they are hidden inside an abstraction, uses of a given semaphore tend to get scattered throughout a program, making it difficult to track them down for purposes of software maintenance.
监视器是 Dijkstra [ Dij72 ]建议使用的一种解决信号量问题的方法。Brinch Hansen [ Bri73 ] 对其进行了更彻底的开发,Hoare [ Hoa74 ] 在 20 世纪 70 年代初对其进行了形式化。监视器已被纳入至少 20 种语言中,其中最具影响力的可能是Concurrent Pascal [ Bri75 ]、Modula (1) [ Wir77b ] 和 Mesa [ LR80 ] 。5它们
Monitors were suggested by Dijkstra [Dij72] as a solution to the problems of semaphores. They were developed more thoroughly by Brinch Hansen [Bri73], and formalized by Hoare [Hoa74] in the early 1970s. They have been incorporated into at least a score of languages, of which Concurrent Pascal [Bri75], Modula (1) [Wir77b], and Mesa [LR80] were probably the most influential.5 They
也强烈影响了 Java 同步机制的设计,我们将在第 13.4.3 节中讨论这一点。
also strongly influenced the design of Java's synchronization mechanisms, which we will consider in Section 13.4.3.
监视器是一个具有操作、内部状态和多个条件变量的模块或对象。在给定的时间点,仅允许给定监视器的一个操作处于活动状态。调用繁忙监视器的线程会自动延迟,直到监视器空闲。任何操作都可以通过等待条件变量来代表其调用线程暂停自身。操作还可以向条件变量发出信号,在这种情况下,其中一个等待线程将恢复,通常是第一个等待的线程。
A monitor is a module or object with operations, internal state, and a number of condition variables. Only one operation of a given monitor is allowed to be active at a given point in time. A thread that calls a busy monitor is automatically delayed until the monitor is free. On behalf of its calling thread, any operation may suspend itself by waiting on a condition variable. An operation may also signal a condition variable, in which case one of the waiting threads is resumed, usually the one that waited first.
Hoare 对监视器的定义是,每个条件变量使用一个线程队列,外加两个簿记队列:入口队列和紧急队列。试图进入繁忙监视器的线程在入口队列中等待。当线程在监视器内执行信号操作,而其他线程正在等待指定条件时,信号线程将在监视器的紧急队列中等待,而相应条件队列中的第一个线程将获得对监视器的控制权。如果没有线程在等待信号条件,则信号操作为无操作。当线程离开监视器时(通过完成其操作或等待条件),它将解除对紧急队列中的第一个线程的阻塞,或者,如果紧急队列为空,则解除对入口队列中的第一个线程的阻塞(如果有)。
Hoare's definition of monitors employs one thread queue for every condition variable, plus two bookkeeping queues: the entry queue and the urgent queue. A thread that attempts to enter a busy monitor waits in the entry queue. When a thread executes a signal operation from within a monitor, and some other thread is waiting on the specified condition, then the signaling thread waits on the monitor's urgent queue and the first thread on the appropriate condition queue obtains control of the monitor. If no thread is waiting on the signaled condition, then the signal operation is a no-op. When a thread leaves a monitor, either by completing its operation or by waiting on a condition, it unblocks the first thread on the urgent queue or, if the urgent queue is empty, the first thread on the entry queue, if any.
许多监视器实现都取消了紧急队列,或者对 Hoare 的原始定义进行了其他更改。从程序员的角度来看,两个主要的变化领域是信号操作的语义和当线程在两个或多个监视器调用的嵌套序列中wait时对互斥的管理。我们将在下面回到这些问题。
Many monitor implementations dispense with the urgent queue, or make other changes to Hoare's original definition. From the programmer's point of view, the two principal areas of variation are the semantics of the signal operation and the management of mutual exclusion when a thread waits inside a nested sequence of two or more monitor calls. We will return to these issues below.
监视器的正确性取决于维护监视器不变量。不变量是一个谓词,它捕获了“监视器的状态是一致的”这一概念。不变量最初和监视器退出时都需要为真。它还需要在每个等待语句中为真,并且在 Hoare 监视器中,在信号操作中也需要为真。对于我们的有界缓冲区示例,合适的不变量将断言full_slots正确指示缓冲区中的项目数,并且这些项目位于编号为next_full到next_empty – 1 (mod SIZE ) 的插槽中。仔细检查图 13.17中的代码会发现不变量最初确实成立,并且任何时候我们修改不变量中提到的变量之一,我们总是在等待、发出信号或从条目返回之前相应地修改其他变量。
Correctness for monitors depends on maintaining a monitor invariant. The invariant is a predicate that captures the notion that “the state of the monitor is consistent.” The invariant needs to be true initially, and at monitor exit. It also needs to be true at every wait statement and, in a Hoare monitor, at signal operations as well. For our bounded buffer example, a suitable invariant would assert that full_slots correctly indicates the number of items in the buffer, and that those items lie in slots numbered next_full through next_empty – 1 (mod SIZE). Careful inspection of the code in Figure 13.17 reveals that the invariant does indeed hold initially, and that any time we modify one of the variables mentioned in the invariant, we always modify the others accordingly before waiting, signaling, or returning from an entry.
Hoare 用信号量来定义监视器。相反,用监视器来定义信号量也很容易(练习 13.18)。这两个定义结合起来证明了信号量和监视器同样强大:它们都可以表达彼此可以表达的所有同步形式。
Hoare defined his monitors in terms of semaphores. Conversely, it is easy to define semaphores in terms of monitors (Exercise 13.18). Together, the two definitions prove that semaphores and monitors are equally powerful: each can express all forms of synchronization expressible with the other.
在大多数监视器语言中,在嵌套的监视器操作序列中等待将释放最内层监视器上的互斥,但会使外层监视器保持锁定状态。如果另一个线程到达相应信号操作的唯一方法是通过相同的外层监视器,则这种情况可能会导致死锁。通常,我们使用术语“死锁”来描述线程集合都在等待彼此,并且它们都无法继续的任何情况。在这种特定情况下,首先进入外层监视器的线程正在等待第二个线程执行信号操作;然而,第二个线程正在等待第一个线程离开监视器。
In most monitor languages, a wait in a nested sequence of monitor operations will release mutual exclusion on the innermost monitor, but will leave the outer monitors locked. This situation can lead to deadlock if the only way for another thread to reach a corresponding signal operation is through the same outer monitor(s). In general, we use the term “deadlock” to describe any situation in which a collection of threads are all waiting for each other, and none of them can proceed. In this specific case, the thread that entered the outer monitor first is waiting for the second thread to execute a signal operation; the second thread, however, is waiting for the first to leave the monitor.
另一种方法是,在内监视器中等待时释放外监视器上的排斥,这种方法被早期的几个单处理器监视器实现所采用,包括 Modula 的原始实现 [ Wir77a ]。然而,它有一个明显的语义缺陷:它要求监视器不变量不仅在监视器出口和(可能)信号操作时成立,而且在任何可能导致等待或(根据霍尔语义)嵌套监视器中的信号的子程序调用时也成立。程序员可能并不完全了解这些调用;它们在源代码中肯定没有在语法上加以区分。
The alternative—to release exclusion on outer monitors when waiting in an inner one—was adopted by several early monitor implementations for uniprocessors, including the original implementation of Modula [Wir77a]. It has a significant semantic drawback, however: it requires that the monitor invariant hold not only at monitor exit and (perhaps) at signal operations, but also at any subroutine call that may result in a wait or (with Hoare semantics) a signal in a nested monitor. Such calls may not all be known to the programmer; they are certainly not syntactically distinguished in the source.
条件临界区出现在并发语言 Edison [ Bri81 ] 中,并且似乎还影响了 Ada 95 和 Java/C# 的同步机制。可以说这些后来的语言融合了监视器和 CCR 的功能,尽管方式不同。
Conditional critical regions appeared in the concurrent language Edison [Bri81], and also seem to have influenced the synchronization mechanisms of Ada 95 and Java/C#. These later languages might be said to blend the features of monitors and CCRs, albeit in different ways.
Ada 中同步的主要机制是在 Ada 83 中引入的,它基于消息传递;我们将在第 13.5 节中描述它。Ada 95 通过受保护对象的概念增强了此机制。受保护对象可以有三种类型的方法:函数、过程和条目。函数只能读取对象的字段;过程和条目可以读取和写入它们。受保护对象上的隐式读写锁可确保潜在冲突的操作在时间上相互排斥:过程或条目获得对对象的独占访问权限;函数可以与其他函数同时操作,但不能与过程或条目同时操作。
The principal mechanism for synchronization in Ada, introduced in Ada 83, is based on message passing; we will describe it in Section 13.5. Ada 95 augments this mechanism with a notion of protected object. A protected object can have three types of methods: functions, procedures, and entries. Functions can only read the fields of the object; procedures and entries can read and write them. An implicit reader–writer lock on the protected object ensures that potentially conflicting operations exclude one another in time: a procedure or entry obtains exclusive access to the object; a function can operate concurrently with other functions, but not with a procedure or entry.
过程和条目在两个重要方面有所不同。首先,条目可以具有布尔表达式保护,调用任务(线程)将在开始执行之前等待该保护(与 CCR 的条件非常相似)。其次,条目支持三种特殊形式的调用:定时调用(在等待指定的时间后中止)、条件调用(如果调用无法立即进行,则执行替代代码)和异步调用(立即开始执行替代代码,但如果调用能够在替代代码完成之前继续进行,则中止该代码)。
Procedures and entries differ from one another in two important ways. First, an entry can have a Boolean expression guard, for which the calling task (thread) will wait before beginning execution (much as it would for the condition of a CCR). Second, an entry supports three special forms of call: timed calls, which abort after waiting for a specified amount of time, conditional calls, which execute alternative code if the call cannot proceed immediately, and asynchronous calls, which begin executing alternative code immediately, but abort it if the call is able to proceed before the alternative completes.
与 CCR 的条件相比,Ada 95 中受保护对象条目上的保护允许更高效的实现,因为它们不必在调用线程的上下文中进行评估。此外,由于所有保护都集中在受保护对象的定义中,因此编译器可以生成代码以尽可能高效地将它们作为一个组进行测试,这是 Kessels [ Kes77 ] 建议的方式。虽然 Ada 任务不能在条目中间等待条件(只能在开头等待),但它可以将自己重新排队到另一个条目上,从而实现大致相同的效果。有界缓冲区的 Ada 95 代码与图 13.18的伪代码非常相似;我们将细节留给练习 13.23。
In comparison to the conditions of CCRs, the guards on entries of protected objects in Ada 95 admit a more efficient implementation, because they do not have to be evaluated in the context of the calling thread. Moreover, because all guards are gathered together in the definition of the protected object, the compiler can generate code to test them as a group as efficiently as possible, in a manner suggested by Kessels [Kes77]. Though an Ada task cannot wait on a condition in the middle of an entry (only at the beginning), it can requeue itself on another entry, achieving much the same effect. Ada 95 code for a bounded buffer would closely resemble the pseudocode of Figure 13.18; we leave the details to Exercise 13.23.
要恢复在给定对象上挂起的线程,其他线程必须从引用同一对象的同步语句或方法中执行预定义方法通知。与wait 一样,通知没有参数。作为对通知调用的响应,语言运行时系统会选择在对象上挂起的任意线程并使其可运行。如果没有这样的线程,则通知为无操作。与 Mesa 一样,有时唤醒给定对象中等待的所有线程可能是合适的;Java为此提供了内置的通知所有方法。
To resume a thread that is suspended on a given object, some other thread must execute the predefined method notify from within a synchronized statement or method that refers to the same object. Like wait, notify has no arguments. In response to a notify call, the language run-time system picks an arbitrary thread suspended on the object and makes it runnable. If there are no such threads, then the notify is a no-op. As in Mesa, it may sometimes be appropriate to awaken all threads waiting in a given object; Java provides a built-in notifyAll method for this purpose.
如果线程正在等待多个条件(即,如果它们的wait嵌入在不同的循环中),则无法保证“正确的”线程将被唤醒。为了确保适当的线程确实被唤醒,程序员可以选择使用notifyAll而不是notify。为了确保唤醒后只有一个线程继续运行,第一个发现其条件已被满足的线程满足条件的线程必须修改对象的状态,以便其他被唤醒的线程在运行时只需返回休眠状态即可。不幸的是,由于所有等待线程最终都会在每次其中一个线程可以运行时重新评估其条件,因此这种多条件问题的“解决方案”可能非常昂贵。
If threads are waiting for more than one condition (i.e., if their waits are embedded in dissimilar loops), there is no guarantee that the “right” thread will awaken. To ensure that an appropriate thread does wake up, the programmer may choose to use notifyAll instead of notify. To ensure that only one thread continues after wakeup, the first thread to discover that its condition has been satisfied must modify the state of the object in such a way that other awakened threads, when they get to run, will simply go back to sleep. Unfortunately, since all waiting threads will end up reevaluating their conditions every time one of them can run, this “solution” to the multiple-condition problem can be quite expensive.
C# 中的同步机制与刚才描述的 Java 机制类似。C# 中的lock语句与 Java 中的synchronized类似。它不能用于标记方法,但可以通过为方法指定Synchronized 属性来实现类似的效果(但有点笨拙) 。使用Pulse和PulseAll方法代替了notify和notifyAll。
The mechanisms for synchronization in C# are similar to the Java mechanisms just described. The C# lock statement is similar to Java's synchronized. It cannot be used to label a method, but a similar effect can be achieved (a bit more clumsily) by specifying a Synchronized attribute for the method. The methods Pulse and PulseAll are used instead of notify and notifyAll.
仅使用同步方法(无锁或同步语句)的 Java 对象与 Mesa 监视器非常相似,其中每个监视器限制一个条件变量(实际上,带有同步语句的对象有时在 Java 中称为监视器)。同样, Java 中以循环中的等待开始的同步语句类似于 CCR,其中已明确重新测试条件。由于通知也是明确的,因此 Java 实现不需要在每次退出临界区时重新评估条件(或唤醒明确这样做的线程),只需在发生通知时重新评估条件即可。
Java objects that use only synchronized methods (no locks or synchronized statements) closely resemble Mesa monitors in which there is a limit of one condition variable per monitor (and in fact objects with synchronized statements are sometimes referred to as monitors in Java). By the same token, a synchronized statement in Java that begins with a wait in a loop resembles a CCR in which the retesting of conditions has been made explicit. Because notify also is explicit, a Java implementation need not reevaluate conditions (or wake up threads that do so explicitly) on every exit from a critical section—only those in which a notify occurs.
我们在13.3.3 节中介绍的 Java 内存模型明确指定了哪些操作可以保证在线程间按顺序执行。它还指定了对于程序执行中的每对读取和写入,是否允许读取返回写入写入的值。
The Java Memory Model, which we introduced in Section 13.3.3, specifies exactly which operations are guaranteed to be ordered across threads. It also specifies, for every pair of reads and writes in a program execution, whether the read is permitted to return the value written by the write.
通俗地说,Java 线程可以缓冲或重新排序其写入(在硬件或软件中),直到它写入一个volatile变量或留下一个监视器(释放锁、离开同步块或等待)。此时,其之前的所有写入都必须对其他线程可见。类似地,允许线程保留其他线程写入的值的缓存副本,直到它读取易失性变量或进入监视器(获取锁、进入同步块或从等待中唤醒)。此时,任何后续读取都必须获取其他线程写入的任何内容的新副本。
Informally, a Java thread is allowed to buffer or reorder its writes (in hardware or in software) until the point at which it writes a volatile variable or leaves a monitor (releases a lock, leaves a synchronized block, or waits). At that point all its previous writes must be visible to other threads. Similarly, a thread is allowed to keep cached copies of values written by other threads until it reads a volatile variable or enters a monitor (acquires a lock, enters a synchronized block, or wakes up from a wait). At that point any subsequent reads must obtain new copies of anything that has been written by other threads.
在没有线程内数据依赖的情况下,编译器可以自由地重新排序普通读取和写入。它还可以将普通读取和写入向下移动到后续volatile读取之后,向上移动到上一个volatile写入之后,或者从上方或下方移动到同步块中。它无法重新排序volatile访问、监视器入口或监视器出口。
The compiler is free to reorder ordinary reads and writes in the absence of intrathread data dependences. It can also move ordinary reads and writes down past a subsequent volatile read, up past a previous volatile write, or into a synchronized block from above or below. It cannot reorder volatile accesses, monitor entry, or monitor exit with respect to one another.
如果编译器可以证明在给定的时间间隔内,一个volatile变量或监视器不会被多个线程使用,那么它可以像普通访问一样重新排序其操作。对于无数据争用程序,这些规则可确保顺序一致性。此外,即使存在争用,Java 实现也能确保对象引用和 32 位及更小数量的读写始终是原子的,并且每次读取都会返回由某个无序写入或某个紧接在前的有序写入写入的值。
If the compiler can prove that a volatile variable or monitor isn't used by more than one thread during a given interval of time, it can reorder its operations like ordinary accesses. For data-race-free programs, these rules ensure the appearance of sequential consistency. Moreover even in the presence of races, Java implementations ensure that reads and writes of object references and of 32-bit and smaller quantities are always atomic, and that every read returns the value written either by some unordered write or by some immediately preceding ordered write.
事实证明,Java 内存模型的形式化是一项极其困难的任务。大部分困难源于希望为具有数据竞争的程序指定有意义的语义。C++11 内存模型(也在13.3.3 节中介绍)通过简单地禁止此类程序来避免这种复杂性。首先近似地,C++ 在内存访问上定义了一种先发生顺序,类似于 Java 中的顺序,然后保证所有冲突访问都按顺序排列的程序的顺序一致性。通过允许程序员在原子变量的单独读写上指定较弱的顺序,引入了适度的额外复杂性;我们在探索 13.42中考虑了这一特性。
Formalization of the Java memory model proved to be a surprisingly difficult task. Most of the difficulty stemmed from the desire to specify meaningful semantics for programs with data races. The C++11 memory model, also introduced in Section 13.3.3, avoids this complexity by simply prohibiting such programs. To first approximation, C++ defines a happens-before ordering on memory accesses, similar to the ordering in Java, and then guarantees sequential consistency for programs in which all conflicting accesses are ordered. Modest additional complexity is introduced by allowing the programmer to specify weaker ordering on individual reads and writes of atomic variables; we consider this feature in Exploration 13.42.
我们考虑过的所有原子性通用机制(信号量、监视器、条件临界区)本质上都是锁的语法变体。需要相互排斥的临界区必须获取和释放相同的锁。相互独立的临界区只有在获取和释放单独的锁时才能并行运行。这给程序员带来了一个不幸的权衡:使用单个锁编写无数据争用程序很容易,但这样的程序无法扩展:随着核心和线程的增加,锁将成为瓶颈,程序性能将停滞不前。为了提高可扩展性,熟练的程序员将他们的程序数据划分为等价类,每个等价类都由单独的锁保护。然后,临界区必须获取每个访问的等价类的锁。如果不同的临界区以不同的顺序获取锁,则会导致死锁。然而,强制执行一个共同的顺序可能很困难,因为我们可能无法预测何时操作开始,最终需要访问哪些数据。更糟糕的是,正确性取决于锁定顺序这一事实意味着基于锁的程序片段无法组合:我们无法采用现有的基于锁的抽象并从新的关键部分中安全地调用它们。
All the general-purpose mechanisms we have considered for atomicity—semaphores, monitors, conditional critical regions—are essentially syntactic variants on locks. Critical sections that need to exclude one another must acquire and release the same lock. Critical sections that are mutually independent can run in parallel only if they acquire and release separate locks. This creates an unfortunate tradeoff for programmers: it is easy to write a data-race-free program with a single lock, but such a program will not scale: as cores and threads are added, the lock will become a bottleneck, and program performance will stagnate. To increase scalability, skillful programmers partition their program data into equivalence classes, each protected by a separate lock. A critical section must then acquire the locks for every accessed equivalence class. If different critical sections acquire locks in different orders, deadlock can result. Enforcing a common order can be difficult, however, because we may not be able to predict, when an operation starts, which data it will eventually need to access. Worse, the fact that correctness depends on locking order means that lock-based program fragments do not compose: we cannot take existing lock-based abstractions and safely call them from within a new critical section.
这些问题表明锁可能是一种太低级的机制。从语义的角度来看,锁和关键部分之间的映射是一个实现细节;我们真正想要的是一个可组合的原子构造。事务内存 (TM) 就是为了提供这一点而尝试的。
These issues suggest that locks may be too low level a mechanism. From a semantic point of view, the mapping between locks and critical sections is an implementation detail; all we really want is a composable atomic construct. Transactional memory (TM) is an attempt to provide exactly that.
长期以来,事务一直被用来实现数据库操作的原子性,并取得了巨大的成功。通常的实现是推测性的:不同线程中的事务同时进行,除非它们在访问数据库中的某些公共记录时发生冲突。在没有冲突的情况下,事务可以完美地并行运行。当发生冲突时,底层系统会在冲突的线程之间进行仲裁。一个线程继续,并希望将其更新提交给数据库;其他线程中止并重新开始(在“回滚”它们迄今为止所做的工作之后)。总体效果是,事务在实现级别实现了显著的并行性,但在程序语义级别似乎以某种全局全序序列化。
Transactions have long been used, with great success, to achieve atomicity for database operations. The usual implementation is speculative: transactions in different threads proceed concurrently unless and until they conflict for access to some common record in the database. In the absence of conflicts, transactions run perfectly in parallel. When conflicts arise, the underlying system arbitrates between the conflicting threads. One gets to continue, and hopefully commit its updates to the database; the others abort and start over (after “rolling back” the work they had done so far). The overall effect is that transactions achieve significant parallelism at the implementation level, but appear to serialize in some global total order at the level of program semantics.
使用更轻量级的事务来实现内存数据结构操作的原子性的想法可以追溯到 1993 年,当时 Herlihy 和 Moss 提出了一个本质上是load_linked/store_conditional指令的多字泛化,如示例 13.29中所述。大约十年后,他们的事务内存(TM) 开始重新受到关注(和更高级别的语义),当时许多研究人员清楚地认识到,只有开发出更简单的编程技术,多核处理器才能成功。
The idea of using more lightweight transactions to achieve atomicity for operations on in-memory data structures dates from 1993, when Herlihy and Moss proposed what was essentially a multiword generalization of the load_linked/ store_conditional, instructions mentioned in Example 13.29. Their transactional memory (TM) began to receive renewed attention (and higher-level semantics) about a decade later, when it became clear to many researchers that multicore processors were going to be successful only with the development of simpler programming techniques.
人们提出了许多不同的 TM 实现,既有硬件实现,也有软件实现。截至 2015 年,IBM 的 z 和 p 系列机器以及英特尔的 x86 最新版本都已提供硬件支持。Haskell 和几种实验性语言和方言均提供语言级支持。C++ 事务扩展的正式提案正在考虑中,预计将于 2017 年 [ Int15 ] 在该语言的下一个修订版中发布。我们还需要几年时间才能知道 TM 在实践中能将并发简化到何种程度,但目前的迹象令人鼓舞。
Many different implementations of TM have been proposed, both in hardware and in software. As of 2015, hardware support is commercially available in IBM's z and p series machines, and in Intel's recent versions of the x86. Language-level support is available in Haskell and in several experimental languages and dialects. A formal proposal for transactional extensions to C++ is under consideration for the next revision of the language, expected in 2017 [Int15]. It will be several years before we know how much TM can simplify concurrency in practice, but current signs are promising.
软件 TM 系统种类繁多,令人吃惊。我们在此概述了一种可能的实现,主要基于 Dice 等人的 TL2 系统 [ DSS06 ] 和 Riegel 等人的 TinySTM 系统 [ FRF08 ]。
There is a surprising amount of variety among software TM systems. We outline one possible implementation here, based, in large part, on the TL2 system of Dice et al. [DSS06] and the TinySTM system of Riegel et al. [FRF08].
每个活动事务都会跟踪它已读取的位置以及它已写入的位置和值。它还维护一个valid_time值,该值指示迄今为止它已读取的所有值已知正确的最新逻辑时间。时间是从全局时钟变量中获取的,每次事务尝试提交时,该变量都会加一。最后,线程共享一个全局所有权记录表(orecs),通过对共享位置的地址进行哈希处理来索引。每个 orec 都包含 (1) 该 orec 所涵盖的(哈希到的)任何位置被更新的最近逻辑时间,或 (2) 当前正尝试向其中一个位置提交更改的事务的 ID t。在情况 (1) 中, orec 被称为无主的;在情况 (2) 中, orec (以及哈希到它的所有位置)被称为由t拥有的。
Every active transaction keeps track of the locations it has read and the locations and values it has written. It also maintains a valid_time value that indicates the most recent logical time at which all of the values it has read so far were known to be correct. Times are obtained from a global clock variable that increases by one each time a transaction attempts to commit. Finally, threads share a global table of ownership records (orecs), indexed by hashing the address of a shared location. Each orec contains either (1) the most recent logical time at which any of the locations covered by (hashing to) that orec was updated, or (2) the ID t of a transaction that is currently trying to commit a change to one of those locations. In case (1), the orec is said to be unowned; in case (2) the orec—and, by extension, all locations that hash to it—is said to be owned by t.
简而言之,事务会缓冲其(推测性)写入,直到准备好提交为止。然后,它会锁定需要写入的所有位置,验证之前读取的所有位置此后均未被覆盖,然后写回并解锁这些位置。在任何时候,事务都知道其所有读取在有效时间都是相互一致的。如果它尝试读取自有效时间以来已更新的新位置,它会尝试将此时间延长至全局时钟的当前值。如果它能够在提交时执行类似的延长,在锁定需要更改的所有位置之后,那么整个事务的总体效果将就像在提交时立即发生一样。
Briefly, a transaction buffers its (speculative) writes until it is ready to commit. It then locks all the locations it needs to write, verifies that all the locations it previously read have not been overwritten since, and then writes back and unlocks the locations. At all times, the transaction knows that all of its reads were mutually consistent at time valid_time. If it ever tries to read a new location that has been updated since valid_time, it attempts to extend this time to the current value of the global clock. If it is able to perform a similar extension at commit time, after having locked all locations it needs to change, then the aggregate effect of the transaction as a whole will be as if it had occurred instantaneously at commit time.
为了实现重试(图 13.19中未显示),我们可以向每个 orec 添加一个可选的线程列表。重试线程会将自己添加到每个位置的列表中在其read_set中,然后对特定于线程的信号量执行P操作。同时,任何对具有等待线程的 orec 提交更改的线程都会对每个线程的信号量执行V。此机制有时会导致不必要的唤醒,但不会影响正确性。唤醒后,线程会将自己从所有线程列表中删除,然后再重新启动其事务。
To implement retry (not shown in Figure 13.19), we can add an optional list of threads to every orec. A retrying thread will add itself to the list of every location in its read_set and then perform a P operation on a thread-specific semaphore. Meanwhile, any thread that commits a change to an orec with waiting threads performs a V on the semaphore of each of those threads. This mechanism will sometimes result in unnecessary wakeups, but these do not impact correctness. Upon wakeup, a thread removes itself from all thread lists before restarting its transaction.
在我们的示例实现中,许多细节都被掩盖了。如果原子块内的代码抛出异常(除abort之外)或执行返回或退出某个周围循环,则示例 13.49中的翻译将无法正常运行。图 13.19的伪代码也没有考虑到事务可能嵌套。
Many subtleties have been glossed over in our example implementation. The translation in Example 13.49 will not behave correctly if code inside the atomic block throws an exception (other than abort) or executes a return or an exit out of some surrounding loop. The pseudocode of Figure 13.19 also fails to consider that transactions may be nested.
TM 设计人员仍在争论其他几个问题。我们应该如何处理事务(I/O、系统调用等)中那些不容易回滚的操作,以及如何防止此类事务调用retry?我们如何阻止程序员创建过大的事务,以至于它们几乎总是相互冲突,并且无法并行运行?程序是否应该能够检测到事务正在中止?事务应如何与锁和非阻塞数据结构交互?事务和非事务代码之间的竞争是否应被视为程序错误?如果是这样,是否应该对可能导致的行为进行任何限制?任何具有生产质量 TM 功能的语言都需要回答这些问题和类似的问题。
Several additional issues are still the subject of debate among TM designers. What should we do about operations inside transactions (I/O, system calls, etc.) that cannot easily be rolled back, and how do we prevent such transactions from ever calling retry? How do we discourage programmers from creating transactions so large they almost always conflict with one another, and cannot run in parallel? Should a program ever be able to detect that transactions are aborting? How should transactions interact with locks and with nonblocking data structures? Should races between transactions and nontransactional code be considered program bugs? If so, should there be any constraints on the behavior that may result? These and similar questions will need to be answered by any production-quality TM-capable language.
在几种共享内存语言中,线程可以对共享数据执行的操作受到限制,使得同步可以隐含在操作本身中,而不是作为单独的显式操作出现。我们已经看到了一个隐式同步的示例:HPF和 Fortran 95 的forall循环(示例 13.10)。forall循环的单独迭代同时进行,在语义上彼此同步:每次迭代都会读取其第一个赋值语句实例中使用的所有数据,然后才会更新其左侧的实例。左侧的更新依次发生在任何迭代读取其第二个赋值语句实例中使用的数据之前,依此类推。为向量机编译forall循环虽然并非易事,但或多或少是直截了当的。然而,在更传统的多处理器上,良好的性能通常取决于高质量的依赖性分析,这使得编译器能够识别循环中的语句实际上不相互依赖并且可以无需同步继续进行的情况。
In several shared-memory languages, the operations that threads can perform on shared data are restricted in such a way that synchronization can be implicit in the operations themselves, rather than appearing as separate, explicit operations. We have seen one example of implicit synchronization already: the forall loop of HPF and Fortran 95 (Example 13.10). Separate iterations of a forall loop proceed concurrently, semantically in lock-step with each other: each iteration reads all data used in its instance of the first assignment statement before any iteration updates its instance of the left-hand side. The left-hand side updates in turn occur before any iteration reads the data used in its instance of the second assignment statement, and so on. Compilation of forall loops for vector machines, while far from trivial, is more or less straightforward. On a more conventional multiprocessor, however, good performance usually depends on high-quality dependence analysis, which allows the compiler to identify situations in which statements within a loop do not in fact depend on one another, and can proceed without synchronization.
依赖性分析在其他语言中也起着至关重要的作用。在侧栏 11.1 中,我们提到了纯函数式语言 Sisal 和 pH(回想一下这些语言中的迭代结构是尾递归的语法糖)。因为这些语言没有副作用,所以它们的结构可以按任何顺序或并发进行评估,只要没有结构尝试使用尚未计算的值。劳伦斯利弗莫尔国家实验室开发的 Sisal 实现使用了大量编译器分析来确定有希望并行执行的结构。它还在数据对象上使用标签来指示对象的值是否已计算。当编译器无法保证在运行时需要某个值时已经计算出来时,生成的代码会使用标签位进行同步、旋转或阻塞,直到它们被正确设置。Sisal 的开发人员在 1992 年声称他们的语言和编译器在性能上可与并行 Fortran 相媲美 [ Can92 ]。
Dependence analysis plays a crucial role in other languages as well. In Sidebar 11.1 we mentioned the purely functional languages Sisal and pH (recall that iterative constructs in these languages are syntactic sugar for tail recursion). Because these languages are side-effect free, their constructs can be evaluated in any order, or concurrently, as long as no construct attempts to use a value that has yet to be computed. The Sisal implementation developed at Lawrence LivermoreNational Lab used extensive compiler analysis to identify promising constructs for parallel execution. It also employed tags on data objects that indicate whether the object's value had been computed yet. When the compiler was unable to guarantee that a value would have been computed by the time it was needed at run time, the generated code used tag bits for synchronization, spinning or blocking until they were properly set. Sisal's developers claimed (in 1992) that their language and compiler rivaled parallel Fortran in performance [Can92].
自动并行化是 20 世纪 80 年代和 90 年代的主要研究课题,最初用于矢量机,后来用于通用机器。它在结构良好的数据并行程序中取得了相当大的成功,主要用于科学应用,并且主要但并非全部用于 Fortran。然而,在更通用、结构不规则的程序中自动识别线程级并行性却很难,消息传递硬件的编译也是如此。该领域的研究仍在继续,并已扩展到 Matlab 和 R 等语言。
Automatic parallelization, first for vector machines and then for general-purpose machines, was a major topic of research in the 1980s and 1990s. It achieved considerable success with well-structured data-parallel programs, largely for scientific applications, and largely but not entirely in Fortran. Automatic identification of thread-level parallelism in more general, irregularly structured programs proved elusive, however, as did compilation for message-passing hardware. Research in this area continues, and has branched out to languages like Matlab and R.
在某些方面, Multilisp 的Future构造类似于Scheme 的内置delay和force (第 6.6.2 节)。然而,Future支持并发,而Delay支持惰性求值:它推迟对其嵌入函数的求值,直到知道需要返回值为止。在 Scheme 中,任何使用Delay表达式都必须用Force包围。相比之下,Multilisp Future上的同步是隐式的 — 没有Force的类似物。
In some ways the future construct of Multilisp resembles the built-in delay and force of Scheme (Section 6.6.2). Where future supports concurrency, however, delay supports lazy evaluation: it defers evaluation of its embedded function until the return value is known to be needed. Any use of a delayed expression in Scheme must be surrounded by force. By contrast, synchronization on a Multilisp future is implicit—there is no analog of force.
C++ async的一个更复杂的变体(示例 13.52中未使用)允许程序员坚持在单独的线程中运行未来,或者,在调用get之前保持未求值状态(此时它将在调用线程中执行)。当使用async时(如我们的示例所示),实现的选择留给运行时系统(就像在 Multilisp 中一样)。
A more complicated variant of the C++ async, not used in Example 13.52, allows the programmer to insist that the future be run in a separate thread—or, alternatively, that it remain unevaluated until get is called (at which point it will execute in the calling thread). When async is used as shown in our example, the choice of implementation is left to the run-time system—as it is in Multilisp.
一些研究人员已经注意到,诸如 Prolog 之类的逻辑语言的回溯搜索也适合并行化。有两种可能的策略。第一种是并行追求规则右侧的子目标。这种策略称为AND 并行。逻辑中的变量一旦初始化就不会再被修改,这一事实确保了AND的并行分支不会互相干扰。第二种策略称为OR 并行;它并行追求替代解决方案。由于它们通常采用不同的统一,因此OR的分支必须使用其变量的单独副本。在如图 12.1所示的搜索树中,AND并行和OR并行在交替的级别创建新的线程。
Several researchers have noted that the backtracking search of logic languages such as Prolog is also amenable to parallelization. Two strategies are possible. The first is to pursue in parallel the subgoals found in the right-hand side of a rule. This strategy is known as AND parallelism. The fact that variables in logic, once initialized, are never subsequently modified ensures that parallel branches of an AND cannot interfere with one another. The second strategy is known as OR parallelism; it pursues alternative resolutions in parallel. Because they will generally employ different unifications, branches of an OR must use separate copies of their variables. In a search tree such as that of Figure 12.1, AND parallelism and OR parallelism create new threads at alternating levels.
OR并行是推测性的:由于只需要在一个分支上成功,因此在其他分支上进行的工作在某种意义上是浪费。但是,当无法满足目标(在这种情况下必须搜索整个树)或以不同方式满足目标所需的执行时间差异很大(在这种情况下同时探索多个分支会减少找到第一个解决方案的预期时间)时,OR 并行会很好地工作。在 Prolog 中, AND和OR并行都是有问题的,因为它们未能遵循语言语义所要求的确定性搜索顺序。Parlog [ Che92 ] 同时支持AND和OR并行,是最著名的并行 Prolog 方言。
OR parallelism is speculative: since success is required on only one branch, work performed on other branches is in some sense wasted. OR parallelism works well, however, when a goal cannot be satisfied (in which case the entire tree must be searched), or when there is high variance in the amount of execution time required to satisfy a goal in different ways (in which case exploring several branches at once reduces the expected time to find the first solution). Both AND and OR parallelism are problematic in Prolog, because they fail to adhere to the deterministic search order required by language semantics. Parlog [Che92], which supports both AND and OR parallelism, is the best known of the parallel Prolog dialects.
共享内存并发在多核处理器和多处理器服务器上已变得无处不在。然而,消息传递仍然主导着分布式和高端计算。超级计算机和大型集群主要使用 Fortran 或 C/C++ 以及 MPI 库包进行编程。分布式计算越来越依赖于基于实现 TCP/IP 互联网标准的库的客户端-服务器抽象层。与共享内存计算一样,还开发了数十种消息传递语言用于特定应用领域,或用于研究或教学目的。
Shared-memory concurrency has become ubiquitous on multicore processors and multiprocessor servers. Message passing, however, still dominates both distributed and high-end computing. Supercomputers and large-scale clusters are programmed primarily in Fortran or C/C++ with the MPI library package. Distributed computing increasingly relies on client–server abstractions layered on top of libraries that implement the TCP/IP Internet standard. As in shared-memory computing, scores of message-passing languages have also been developed for particular application domains, or for research or pedagogical purposes.
更深入地
IN MORE DEPTH
配套站点探讨了基于消息的并发中的三个核心问题——命名、发送和接收。名称可以直接引用进程、与进程关联的某些通信资源(通常称为条目或端口),或独立的套接字或通道抽象。发送操作可能完全是异步的,在这种情况下,发送方在底层系统尝试传递消息时继续,或者发送方可能等待,通常是等待确认收据或返回答复。接收操作可以显式执行,也可以隐式触发某些先前指定的处理程序例程的执行。当隐式接收与等待答复的发送方相结合时,这种组合通常称为远程过程调用(RPC)。除了消息传递库之外,RPC 系统通常还依赖于一种称为存根编译器的语言感知工具。
Three central issues in message-based concurrency—naming, sending, and receiving—are explored on the companion site. A name may refer directly to a process, to some communication resource associated with a process (often called an entry or port), or to an independent socket or channel abstraction. A send operation may be entirely asynchronous, in which case the sender continues while the underlying system attempts to deliver the message, or the sender may wait, typically for acknowledgment of receipt or for the return of a reply. A receive operation, for its part, may be executed explicitly, or it may implicitly trigger execution of some previously specified handler routine. When implicit receipt is coupled with senders waiting for replies, the combination is typically known as remote procedure call (RPC). In addition to message-passing libraries, RPC systems typically rely on a language-aware tool known as a stub compiler.
并发和并行在现代计算机系统中无处不在。可以肯定地说,当今大多数计算机研究和开发都以某种形式涉及并发。高端计算机系统一直都是并行的,多核 PC 和手机现在无处不在。即使在单处理器上,图形和网络应用程序通常也是并发的。
Concurrency and parallelism have become ubiquitous in modern computer systems. It is probably safe to say that most computer research and development today involves concurrency in one form or another. High-end computer systems have always been parallel, and multicore PCs and cellphones are now ubiquitous. Even on uniprocessors, graphical and networked applications are typically concurrent.
本章介绍了并发编程,重点介绍了编程语言问题。我们首先概述了并发的动机和现代多处理器的体系结构。然后,我们概述了并发软件的基础知识,包括通信、同步以及线程的创建和管理。我们区分了共享内存和消息传递通信和同步模型,以及基于语言和基于库的并发实现。
In this chapter we have provided an introduction to concurrent programming with an emphasis on programming language issues. We began with an overview of the motivations for concurrency and of the architecture of modern multiprocessors. We then surveyed the fundamentals of concurrent software, including communication, synchronization, and the creation and management of threads. We distinguished between shared-memory and message-passing models of communication and synchronization, and between language- and library-based implementations of concurrency.
我们对线程创建和管理的调查描述了创建线程的六种不同构造:co-begin、并行循环、launch-at-elaboration、fork/join、隐式接收和早期回复。其中fork/join是最常见的;它存在于许多语言中,以及基于库的软件包中,例如 MPI 和 OpenMP。RPC 系统通常在内部使用fork/join来实现隐式接收。无论线程创建机制如何,大多数并发编程系统都在操作系统级进程集合之上实现其语言级或库级线程,操作系统以类似的方式在硬件核心集合之上实现这些进程。我们分阶段构建了示例实现,从单处理器上的协程开始,然后添加就绪列表和调度程序,然后添加用于抢占的计时器,最后在多个核心上进行并行调度。
Our survey of thread creation and management described some six different constructs for creating threads: co-begin, parallel loops, launch-at-elaboration, fork/join, implicit receipt, and early reply. Of these fork/join is the most common; it is found in a host of languages, and in library-based packages such as MPI and OpenMP. RPC systems typically use fork/join internally to implement implicit receipt. Regardless of the thread-creation mechanism, most concurrent programming systems implement their language- or library-level threads on top of a collection of OS-level processes, which the operating system implements in a similar manner on top of a collection of hardware cores. We built our sample implementation in stages, beginning with coroutines on a uniprocessor, then adding a ready list and scheduler, then timers for preemption, and finally parallel scheduling on multiple cores.
本章的大部分内容集中在共享内存编程模型上,尤其是同步。我们区分了原子性和条件同步,以及忙等待和基于调度程序的实现。在忙等待机制中,我们特别研究了自旋锁和屏障。在基于调度程序的机制中,我们研究了信号量、监视器和条件临界区。在这三种机制中,信号量是最简单的,并且仍然被广泛使用,特别是在操作系统中。监视器和条件临界区提供了更好的封装和抽象,但不适合在库中实现。条件临界区可能被认为提供了最令人愉快的编程模型,但通常不能像监视器那样有效地实现。
The bulk of the chapter focused on shared-memory programming models, and on synchronization in particular. We distinguished between atomicity and condition synchronization, and between busy-wait and scheduler-based implementations. Among busy-wait mechanisms we looked in particular at spin locks and barriers. Among scheduler-based mechanisms we looked at semaphores, monitors, and conditional critical regions. Of the three, semaphores are the simplest, and remain widely used, particularly in operating systems. Monitors and conditional critical regions provide better encapsulation and abstraction, but are not amenable to implementation in a library. Conditional critical regions might be argued to provide the most pleasant programming model, but cannot in general be implemented as efficiently as monitors.
我们还考虑了并行函数式语言和高性能 Fortran 等数据并行语言的并行化编译器提供的隐式同步。对于以函数式风格编写的程序,我们考虑了Multilisp 引入的未来机制,该机制随后被纳入许多其他语言,包括 Java、C#、C++ 和 Scala。
We also considered the implicit synchronization provided by parallel functional languages and by parallelizing compilers for such data-parallel languages as High Performance Fortran. For programs written in a functional style, we considered the future mechanism introduced by Multilisp and subsequently incorporated into many other languages, including Java, C#, C++, and Scala.
作为基于锁的原子性的替代方案,我们考虑了非阻塞数据结构,它可以避免由于不合时宜的抢占和页面错误而导致的性能异常。对于某些常见结构,即使在常见情况下,非阻塞算法也可以胜过锁。不幸的是,它们往往非常微妙且难以创建。
As an alternative to lock-based atomicity, we considered nonblocking data structures, which avoid performance anomalies due to inopportune preemption and page faults. For certain common structures, nonblocking algorithms can outperform locks even in the common case. Unfortunately, they tend to be extraordinarily subtle and difficult to create.
事务内存 (TM) 最初被认为是一种为任意数据结构构建非阻塞代码的通用方法。然而,最近的大多数实现都放弃了非阻塞保证,而是专注于指定原子性而无需设计显式锁定协议的能力。与条件临界区一样,TM 牺牲了性能来换取可编程性。现在,各种语言都有原型实现,并且有多种商业指令集的硬件支持。
Transactional memory (TM) was originally conceived as a general-purpose means of building nonblocking code for arbitrary data structures. Most recent implementations, however, have given up on nonblocking guarantees, focusing instead on the ability to specify atomicity without devising an explicit locking protocol. Like conditional critical regions, TM sacrifices performance for the sake of programmability. Prototype implementations are now available for a wide variety of languages, with hardware support in several commercial instruction sets.
我们关于消息传递的部分(主要在配套网站上)从多个库和语言中选取了示例,并考虑了进程如何命名彼此、发送消息时阻塞多长时间以及接收是隐式的还是显式的。分布式计算越来越依赖于远程过程调用,它将远程调用发送(等待回复)与隐式消息接收相结合。
Our section on message passing, mostly on the companion site, drew examples from several libraries and languages, and considered how processes name each other, how long they block when sending a message, and whether receipt is implicit or explicit. Distributed computing increasingly relies on remote procedure calls, which combine remote-invocation send (wait for a reply) with implicit message receipt.
与前面的章节一样,我们看到了许多语言设计和语言实现相互影响的情况。一些机制(仙人掌堆栈、条件临界区、基于内容的消息筛选)非常复杂,许多语言设计者选择不提供它们。其他机制(Ada 样式的参数模式)是专门为促进有效的实现技术而开发的。而在其他情况下(无等待发送的语义、监视器内的阻塞),实现问题在更大的权衡中起着重要作用。
As in previous chapters, we saw many cases in which language design and language implementation influence one another. Some mechanisms (cactus stacks, conditional critical regions, content-based message screening) are sufficiently complex that many language designers have chosen not to provide them. Other mechanisms (Ada-style parameter modes) have been developed specifically to facilitate an efficient implementation technique. And in still other cases (the semantics of no-wait send, blocking inside a monitor), implementation issues play a major role in some larger set of tradeoffs.
尽管并发语言设计的历史非常悠久,但直到最近,大多数多线程程序都依赖于基于库的线程包。然而,即使是 C 和 C++ 现在也明确是并行的,很难想象任何新的专为纯顺序执行而设计的语言。截至 2015 年,显式并行语言尚未严重破坏 MPI 在高端科学计算领域的主导地位,尽管这种情况在未来几年也可能会发生变化。
Despite the very long history of concurrent language design, until recently most multithreaded programs relied on library-based thread packages. Even C and C++ are now explicitly parallel, however, and it is hard to imagine any new languages being designed for purely sequential execution. As of 2015, explicitly parallel languages have yet to seriously undermine the dominance of MPI for high-end scientific computing, though this, too, may change in coming years.
13.1 给出一个“良性”竞争条件的例子——其结果会影响程序行为,但不会影响正确性。
13.1 Give an example of a “benign” race condition—one whose outcome affects program behavior, but not correctness.
13.2 我们已经定义了线程包的就绪列表,以包含所有可运行但未运行的线程,并使用单独的变量来标识当前正在运行的线程。我们是否可以同样轻松地定义就绪列表以包含所有可运行的线程,并理解列表开头的线程正在运行?(提示:考虑多处理器。)
13.2 We have defined the ready list of a thread package to contain all threads that are runnable but not running, with a separate variable to identify the currently running thread. Could we just as easily have defined the ready list to contain all runnable threads, with the understanding that the one at the head of the list is running? (Hint: Think about multiprocessors.)
13.3 假设你正在编写代码来管理一个将在多个并发线程之间共享的哈希表。假设对该表的操作必须是原子的。你可以使用一个互斥锁来保护整个表,或者你可以设计一个方案,每个哈希表存储桶一个锁。哪种方法可能效果更好,在什么情况下?为什么?
13.3 Imagine you are writing the code to manage a hash table that will be shared among several concurrent threads. Assume that operations on the table need to be atomic. You could use a single mutual exclusion lock to protect the entire table, or you could devise a scheme with one lock per hashtable bucket. Which approach is likely to work better, under what circumstances? Why?
13.4 典型的自旋锁只保存一位数据,但需要存储一个完整的字,因为在硬件中,只有完整的字才能被原子地读取、修改和写入。但是,请考虑上一个练习中的哈希表。如果我们选择对表的每个存储桶使用单独的锁,请解释如何实现“两级”锁定方案,该方案将表作为一个整体的传统自旋锁与每个存储桶的单个锁定信息相结合。解释为什么这种方案可能是可取的,特别是在具有外部链接的表中。
13.4 The typical spin lock holds only one bit of data, but requires a full word of storage, because only full words can be read, modified, and written atomically in hardware. Consider, however, the hash table of the previous exercise. If we choose to employ a separate lock for each bucket of the table, explain how to implement a “two-level” locking scheme that couples a conventional spin lock for the table as a whole with a single bit of locking information for each bucket. Explain why such a scheme might be desirable, particularly in a table with external chaining.
13.5从 示例 13.29和13.30中汲取灵感,使用compare_and_swap设计一个堆栈的非阻塞链表实现。(当CAS首次在 IBM 370 架构上推出时,该算法是驱动应用之一 [ Tre86 ]。)
13.5 Drawing inspiration from Examples 13.29 and 13.30, design a nonblocking linked-list implementation of a stack using compare_and_swap. (When CAS was first introduced, on the IBM 370 architecture, this algorithm was one of the driving applications [Tre86].)
13.6 在上一个练习的基础上,假设堆栈节点是动态分配的。如果我们读取一个指针,然后被延迟(例如,由于抢占),则指针指向的节点可能会被回收,然后重新分配用于其他目的。随后的比较和交换可能会成功,而从逻辑上讲它不应该成功。这个问题被称为ABA 问题。
给出一个具体的例子——两个或多个线程中的操作交错——其中 ABA 问题可能导致堆栈行为不正确。解释为什么这种行为不会发生在具有自动垃圾收集的系统中。建议在具有手动存储管理的系统中可以采取哪些措施来避免这种情况。
13.6 Building on the previous exercise, suppose that stack nodes are dynamically allocated. If we read a pointer and then are delayed (e.g., due to preemption), the node to which the pointer refers may be reclaimed and then reallocated for a different purpose. A subsequent compare-and-swap may then succeed when logically it should not. This issue is known as the ABA problem.
Give a concrete example—an interleaving of operations in two or more threads—where the ABA problem may result in incorrect behavior for your stack. Explain why this behavior cannot occur in systems with automatic garbage collection. Suggest what might be done to avoid it in systems with manual storage management.
13.7 我们在第 13.3.2 节中指出 包括 ARM、MIPS 和 Power 在内的多种处理器都提供了compare_and_swap (CAS)的替代方案,称为load_linked /store_conditional (LL/SC)。load_linked指令将内存位置加载到寄存器中,并将某些簿记信息存储到隐藏的处理器寄存器中。store_conditional指令将寄存器存储回内存位置,但前提是自执行load_linked以来该位置未被任何其他处理器修改。与compare_and_swap一样,store_conditional返回是否成功的指示。
13.7 We noted in Section 13.3.2 that several processors, including the ARM, MIPS, and Power, provide an alternative to compare_and_swap (CAS) known as load_linked/store_conditional (LL/SC). A load_linked instruction loads a memory location into a register and stores certain bookkeeping information into hidden processor registers. A store_conditional instruction stores the register back into the memory location, but only if the location has not been modified by any other processor since the load_linked was executed. Like compare_and_swap, store_conditional returns an indication of whether it succeeded or not.
(a) 使用LL/SC重写示例 13.29的代码序列。
(a) Rewrite the code sequence of Example 13.29 using LL/SC.
(b) 在大多数机器上,SC指令可能由于多种“虚假”原因而失败,包括页面错误、缓存未命中或自匹配LL以来发生中断。程序员必须采取哪些步骤来确保算法在遇到此类故障时正常工作?
(b) On most machines, an SC instruction can fail for any of several “spurious” reasons, including a page fault, a cache miss, or the occurrence of an interrupt in the time since the matching LL. What steps must a programmer take to make sure that algorithms work correctly in the face of such failures?
(c)讨论 LL/SC和CAS的相对优势。考虑如何在缓存一致性多处理器上实现它们。是否存在一种可以工作而另一种不工作的情况?(提示:考虑线程可能需要接触多个内存位置的算法。还要考虑内存位置的内容可能被更改然后恢复的算法,如上一个练习中所述。)
(c) Discuss the relative advantages of LL/SC and CAS. Consider how they might be implemented on a cache-coherent multiprocessor. Are there situations in which one would work but the other would not? (Hints: Consider algorithms in which a thread may need to touch more than one memory location. Also consider algorithms in which the contents of a memory location might be changed and then restored, as in the previous exercise.)
13.8 从图 13.8中的测试和设置锁开始,实现忙等待代码,允许读者并发访问数据结构。写者仍然需要锁定读者和其他写者。您可以使用任何合理的原子指令(例如LL/SC)。考虑公平性问题。特别是,如果总是有读者对访问数据结构感兴趣,您的算法应该确保写者不会被永远锁定。
13.8 Starting with the test-and-test_and_set lock of Figure 13.8, implement busy-wait code that will allow readers to access a data structure concurrently. Writers will still need to lock out both readers and other writers. You may use any reasonable atomic instruction(s) (e.g., LL/SC). Consider the issue of fairness. In particular, if there are always readers interested in accessing the data structure, your algorithm should ensure that writers are not locked out forever.
13.9 假设 Java 内存模型,
13.9 Assuming the Java memory model,
(a)解释为什么在 图 13.11中将X和Y标记为volatile是不够的。
(a) Explain why it is not sufficient in Figure 13.11 to label X and Y as volatile.
(b) 解释为什么在同一个图中,将C的读取(类似地,D的读取)封闭在某些公共共享对象O 的同步块中就足够了。
(b) Explain why it is sufficient, in that same figure, to enclose C's reads (and similarly those of D) in a synchronized block for some common shared object O.
(c)解释为什么在 示例 13.31中将inspected和X都标记为volatile就足够了,而不能只标记一个。
(c) Explain why it is sufficient, in Example 13.31, to label both inspected and X as volatile, but not to label only one.
(提示:您可能会发现查阅 Doug Lea 的 Java 内存模型“编译器编写者指南”很有用,网址为gee.cs.oswego.edu/dl/jmm/cookbook. html)。
(Hint: You may find it useful to consult Doug Lea's Java Memory Model “Cookbook for Compiler Writers,” at gee.cs.oswego.edu/dl/jmm/cookbook. html).
13.10 在 x86 上实现示例 13.30中的非阻塞队列。(完整的伪代码可以在 Michael 和 Scott 的论文 [ MS98 ] 中找到。)
您是否需要隔离指令来确保一致性?如果您可以使用合适的硬件,请将您的代码移植到具有更宽松内存模型的机器(例如 ARM 或 Power)。您需要哪些新的隔离指令或原子引用?
13.10 Implement the nonblocking queue of Example 13.30 on an x86. (Complete pseudocode can be found in the paper by Michael and Scott [MS98].)
Do you need fence instructions to ensure consistency? If you have access to appropriate hardware, port your code to a machine with a more relaxed memory model (e.g., ARM or Power). What new fences or atomic references do you need?
13.11考虑 图 13.19中的软件事务内存的实现。
13.11 Consider the implementation of software transactional memory in Figure 13.19.
(a) 您将如何实现read_set、write_map和lock_map数据结构?您不仅希望最小化插入和查找操作的成本,还希望最小化 (1) 在事务结束时将表“清零”,以便可以再次使用;以及 (2) 如果表太满,则扩展表。
(a) How would you implement the read_set, write_map, and lock_map data structures? You will want to minimize the cost not only of insert and lookup operations but also of (1) “zeroing out” the table at the end of a transaction, so it can be used again; and (2) extending the table if it becomes too full.
(b) 验证例程在两个不同的地方被调用。内联扩展这些调用并根据调用上下文对其进行自定义。您可以实现哪些优化?
(b) The validate routine is called in two different places. Expand these calls in-line and customize them to the calling context. What optimizations can you achieve?
(c) 优化提交例程,利用这样一个事实:如果自valid_time以来没有其他事务提交,则不需要最终验证。
(c) Optimize the commit routine to exploit the fact that a final validation is unnecessary if no other transaction has committed since valid_time.
(d) 通过观察finally子句中的for循环实际上需要遍历 orecs,而不是遍历地址(如果多个地址散列到同一个 orec,则可能会有差异),进一步优化提交。理想情况下, lock_map应该保存哪些数据?
(d) Further optimize commit by observing that the for loop in the finally clause really needs to iterate over orecs, not over addresses (there may be a difference, if more than one address hashes to the same orec). What data, ideally, should lock_map hold?
13.12 可以公平地指责示例 13.35中的代码抽象性较差。如果我们将desire_condition设为委托(子例程或对象闭包),是否可以将其作为额外参数传递,并将信号和scheduler_lock管理移到sleep_on中?(提示:考虑图 13.15中 P 操作的代码。)
13.12 The code of Example 13.35 could fairly be accused of displaying poor abstraction. If we make desired_condition a delegate (a subroutine or object closure), can we pass it as an extra parameter, and move the signal and scheduler_lock management inside sleep_on? (Hint: Consider the code for the P operation in Figure 13.15.)
13.13 图 13.13中用于使调度程序代码可重入的机制对应用程序的所有调度数据结构使用单个操作系统提供的锁。除其他外,此机制可防止不同处理器上的线程对不相关的信号量执行P或V操作,即使这些操作都不需要阻塞。您能否设计出另一种用于调度程序相关操作的同步机制,该机制允许更高程度的并发性,但仍然正确?
13.13 The mechanism used in Figure 13.13 to make scheduler code reentrant employs a single OS-provided lock for all the scheduling data structures of the application. Among other things, this mechanism prevents threads on separate processors from performing P or V operations on unrelated semaphores, even when none of the operations needs to block. Can you devise another synchronization mechanism for scheduler-related operations that admits a higher degree of concurrency but that is still correct?
13.14 说明如何将基于锁的并发集实现为单向链接排序列表。您的实现应支持插入、查找和删除操作,并应允许对列表的不同部分进行并发操作(因此对整个列表使用单个锁是不够的)。(提示:您将需要使用“行走锁”习语,其中获取和释放操作以非后进先出顺序交错进行。)
13.14 Show how to implement a lock-based concurrent set as a singly linked sorted list. Your implementation should support insert, find, and remove operations, and should permit operations on separate portions of the list to occur concurrently (so a single lock for the entire list will not suffice). (Hint: You will want to use a “walking lock” idiom in which acquire and release operations are interleaved in non-LIFO order.)
13.15 (困难)实现上一个练习中集合的非阻塞版本。(提示:你可能会发现插入很容易,但删除很难。考虑一种惰性删除机制,其中清理(物理删除节点)可能在删除的逻辑完成后发生。有关详细信息,请参阅 Harris [ Har01 ]的工作。)
13.15 (Difficult) Implement a nonblocking version of the set of the previous exercise. (Hint: You will probably discover that insertion is easy but deletion is hard. Consider a lazy deletion mechanism in which cleanup [physical removal of a node] may occur well after logical completion of the removal. For further details see the work of Harris [Har01].)
13.16 为了使自旋锁在多道程序多处理器上有用,可能需要确保在临界区中间不会有任何进程被抢占。这样,在用户空间中自旋就总是安全的,因为可以保证持有锁的进程在其他处理器上运行,而不是被抢占并可能需要当前处理器。解释为什么操作系统设计者可能不想让用户进程能够任意禁用抢占。(提示:考虑公平性和多用户。)你能建议一种解决这个问题的方法吗?(在 Kontothanassis、Wisniewski 和 Scott 的论文 [ KWS97 ] 中可以找到对几种可能解决方案的引用。)
13.16 To make spin locks useful on a multiprogrammed multiprocessor, one might want to ensure that no process is ever preempted in the middle of a critical section. That way it would always be safe to spin in user space, because the process holding the lock would be guaranteed to be running on some other processor, rather than preempted and possibly in need of the current processor. Explain why an operating system designer might not want to give user processes the ability to disable preemption arbitrarily. (Hint: Think about fairness and multiple users.) Can you suggest a way to get around the problem? (References to several possible solutions can be found in the paper by Kontothanassis, Wisniewski, and Scott [KWS97].)
13.17 说明如何使用信号量构建基于调度程序的n线程屏障。
13.17 Show how to use semaphores to construct a scheduler-based n-thread barrier.
13.18 证明监视器和信号量同样强大。也就是说,用一个来实现另一个。在基于监视器的信号量实现中,你的监视器不变量是什么?
13.18 Prove that monitors and semaphores are equally powerful. That is, use each to implement the other. In the monitor-based implementation of semaphores, what is your monitor invariant?
13.19 说明如何使用二进制信号量实现通用信号量。
13.19 Show how to use binary semaphores to implement general semaphores.
13.20 在示例 13.38(图 13.15 )中,假设我们将过程 P的中间四行替换为if SN = 0 sleep_on(SQ) SN -:= 1,将过程 V 的中间四行替换为S.N +:= 1 if SQ is nonempty enqueue(ready_list, dequeue(SQ))这个新版本的问题是什么?解释它与第 13.4.1 节中的提示和绝对值问题有何关联。
13.20 In Example 13.38 (Figure 13.15), suppose we replaced the middle four lines of procedure P with
if S.N = 0
sleep_on(S.Q)
S.N -:= 1
and the middle four lines of procedure V with
S.N +:= 1
if S.Q is nonempty
enqueue(ready_list, dequeue(S.Q))
What is the problem with this new version? Explain how it connects to the question of hints and absolutes in Section 13.4.1.
13.21 假设每个监视器都有一个单独的互斥锁,这样不同的线程可以同时在不同的监视器中运行,并且当线程在嵌套调用中wait时,我们希望在内监视器和外监视器上都释放互斥。当线程被唤醒时,它将需要重新获取外锁。我们如何确保它能够这样做?(提示:考虑获取锁的顺序,并准备放弃霍尔语义。有关更多提示,请参阅 Wettstein [ Wet78 ]。)
13.21 Suppose that every monitor has a separate mutual exclusion lock, so that different threads can run in different monitors concurrently, and that we want to release exclusion on both inner and outer monitors when a thread waits in a nested call. When the thread awakens it will need to reacquire the outer locks. How can we ensure its ability to do so? (Hint: Think about the order in which to acquire locks, and be prepared to abandon Hoare semantics. For further hints, see Wettstein [Wet78].)
13.22 说明如何使用条件临界区实现通用信号量,其中所有线程都等待相同条件,从而避免无效唤醒的开销。
13.22 Show how general semaphores can be implemented with conditional critical regions in which all threads wait for the same condition, thereby avoiding the overhead of unproductive wake-ups.
13.23 使用 Ada 95 的受保护对象机制为有界缓冲区编写代码。
13.23 Write code for a bounded buffer using the protected object mechanism of Ada 95.
13.24使用 synchronized语句或方法重复上一个Java练习。尽量使你的解决方案尽可能简单和概念清晰。你可能想要使用notifyAll。
13.24 Repeat the previous exercise in Java using synchronized statements or methods. Try to make your solution as simple and conceptually clear as possible. You will probably want to use notifyAll.
13.25 为上一个练习给出一个更有效的解决方案,避免使用notifyAll。(警告:很容易观察到缓冲区不可能同时为满和空,因此假设等待线程要么全部是生产者,要么全部是消费者。然而,情况不一定如此:如果缓冲区成为哪怕是暂时的性能瓶颈,则可能会有任意数量的等待线程,包括生产者和消费者。)
13.25 Give a more efficient solution to the previous exercise that avoids the use of notifyAll. (Warning: It is tempting to observe that the buffer can never be both full and empty at the same time, and to assume therefore that waiting threads are either all producers or all consumers. This need not be the case, however: if the buffer ever becomes even a temporary performance bottleneck, there may be an arbitrary number of waiting threads, including both producers and consumers.)
13.26使用Java Lock变量重复上面的练习。
13.26 Repeat the previous exercise using Java Lock variables.
13.27 解释如何使用边栏 10.3 中简要提到的逃逸分析来降低Java 中某些同步语句和方法的成本。
13.27 Explain how escape analysis, mentioned briefly in Sidebar 10.3, could be used to reduce the cost of certain synchronized statements and methods in Java.
13.28 哲学家就餐问题[ Dij72 ] 是一个经典的同步练习(图 13.20)。五位哲学家围坐在一张圆桌旁。桌子中央放着一大盘意大利面条。每位哲学家反复思考一会儿,然后吃一会儿,时间间隔由他或她自己选择。每对相邻的哲学家之间的桌子上有一把餐叉。要吃饭,哲学家需要两把相邻的餐叉:左边的和右边的。因为他们共用一个餐叉,所以相邻的哲学家不能同时吃饭。
写出哲学家就餐问题的解决方案,其中每个哲学家由一个进程表示,餐叉由共享数据表示。使用信号量、监视器或条件临界区同步对餐叉的访问。尝试最大化并发性。
13.28 The dining philosophers problem [Dij72] is a classic exercise in synchronization (Figure 13.20). Five philosophers sit around a circular table. In the center is a large communal plate of spaghetti. Each philosopher repeatedly thinks for a while and then eats for a while, at intervals of his or her own choosing. On the table between each pair of adjacent philosophers is a single fork. To eat, a philosopher requires both adjacent forks: the one on the left and the one on the right. Because they share a fork, adjacent philosophers cannot eat simultaneously.
Write a solution to the dining philosophers problem in which each philosopher is represented by a process and the forks are represented by shared data. Synchronize access to the forks using semaphores, monitors, or conditional critical regions. Try to maximize concurrency.
13.29 在上一个练习中,你可能已经注意到,就餐的哲学家很容易陷入死锁。人们不得不担心,五个哲学家可能会同时拿起右手边的叉子,然后永远等着左手边的邻居吃完饭。
讨论你能想到的解决死锁问题的尽可能多的策略。你能描述一个解决方案,证明任何哲学家都不可能永远挨饿吗?你能描述一个在严格意义上公平的解决方案吗(即,从长远来看,没有一个哲学家比其他哲学家有更多吃饭的机会)?有关特别优雅的解决方案,请参阅 Chandy 和 Misra 的论文 [ CM84 ]。
13.29 In the previous exercise you may have noticed that the dining philosophers are prone to deadlock. One has to worry about the possibility that all five of them will pick up their right-hand forks simultaneously, and then wait forever for their left-hand neighbors to finish eating.
Discuss as many strategies as you can think of to address the deadlock problem. Can you describe a solution in which it is provably impossible for any philosopher to go hungry forever? Can you describe a solution that is fair in a strong sense of the word (i.e., in which no one philosopher gets more chance to eat than some other over the long term)? For a particularly elegant solution, see the paper by Chandy and Misra [CM84].
13.30 在某些并发编程系统中,全局变量由所有线程共享。在其他系统中,每个新创建的线程都有全局变量的单独副本,通常将其初始化为创建线程的全局变量的值。在这种私有全局变量方法下,共享数据必须从特殊堆中分配。在其他编程系统中,程序员可以指定哪些全局变量是私有的,哪些是共享的。
讨论私有全局变量和共享全局变量之间的权衡。对于哪种程序,你更愿意使用哪种全局变量?你将如何实现每种全局变量?有些选项比其他选项更难实现吗?你的答案在多大程度上取决于操作系统提供的进程的性质?
13.30 In some concurrent programming systems, global variables are shared by all threads. In others, each newly created thread has a separate copy of the global variables, commonly initialized to the values of the globals of the creating thread. Under this private globals approach, shared data must be allocated from a special heap. In still other programming systems, the programmer can specify which global variables are to be private and which are to be shared.
Discuss the tradeoffs between private and shared global variables. Which would you prefer to have available, for which sorts of programs? How would you implement each? Are some options harder to implement than others? To what extent do your answers depend on the nature of processes provided by the operating system?
13.31用 Java 重写示例 13.51。
13.31 Rewrite Example 13.51 in Java.
13.32逻辑语言中的 AND并行类似于函数式语言(例如 Multilisp)中参数的并行计算。OR 并行有类似的类似物吗?(提示:考虑特殊形式 [第 11.5 节]。)您能建议一种在 Multilisp 中获得OR并行效果的方法吗?
13.32 AND parallelism in logic languages is analogous to the parallel evaluation of arguments in a functional language (e.g., Multilisp). Does OR parallelism have a similar analog? (Hint: Think about special forms [Section 11.5].) Can you suggest a way to obtain the effect of OR parallelism in Multilisp?
13.33 在第 13.4.5 节中,我们声称Prolog 中的AND并行和OR并行都是有问题的,因为它们没有遵循语言语义所要求的确定性搜索顺序。详细说明这一说法。具体可能出现什么问题?
13.33 In Section 13.4.5 we claimed that both AND parallelism and OR parallelism were problematic in Prolog, because they failed to adhere to the deterministic search order required by language semantics. Elaborate on this claim. What specifically can go wrong?
13.34–13.38 更深入。
13.34–13.38 In More Depth.
13.39 x86 指令集的 MMX、SSE 和 AVX 扩展以及 Power 指令集的 AltiVec 扩展使矢量运算可用于通用代码。了解这些指令并研究它们的历史。它们用于哪种类型的代码?它们与矢量超级计算机有何关系?与现代图形处理器有何关系?
13.39 The MMX, SSE, and AVX extensions to the x86 instruction set and the AltiVec extensions to the Power instruction set make vector operations available to general-purpose code. Learn about these instructions and research their history. What sorts of code are they used for? How are they related to vector supercomputers? To modern graphics processors?
13.40 “500 强”名单(top500.org)维护着世界上 500 台最强大的计算机的长期信息,这些计算机是根据 Linpack 性能基准进行测量的。浏览该网站。特别注意所部署机器类型的历史趋势。你能解释这些趋势吗?你能找到多少超级计算机技术进入主流和反之亦然的案例?
13.40 The “Top 500” list (top500.org) maintains information, over time, on the 500 most powerful computers in the world, as measured on the Linpack performance benchmark. Explore the site. Pay particular attention to the historical trends in the kinds of machines deployed. Can you explain these trends? How many cases can you find of supercomputer technology moving into the mainstream, and vice versa?
13.41 在第 13.3.3 节中,我们指出不同的处理器提供不同级别的内存一致性和不同的机制来在需要时强制进行额外排序。了解有关这些硬件内存模型的更多信息。您可能希望从 Adve 和 Gharachorloo 的教程 [ AG96 ]开始。
13.41 In Section 13.3.3 we noted that different processors provide different levels of memory consistency and different mechanisms to force additional ordering when needed. Learn more about these hardware memory models. You might want to start with the tutorial by Adve and Gharachorloo [AG96].
13.42 在第 13.3.3和13.4.3节中,我们对 Java 和 C++ 内存模型进行了非常高级的总结。了解它们的细节。同时研究 Ada 和 C# 的(更松散指定的)模型。它们之间有何区别?它们在各种实际机器上的实现效率如何?实现者面临哪些挑战?对于 Java,探索语言原始定义中围绕内存模型出现的争议(在 Java 5 中更新 - 有关讨论,请参阅 Manson 等人的论文 [ MPA05 ])。对于 C++,特别注意在原子变量的加载和存储上指定弱一致性的能力。
13.42 In Sections 13.3.3 and 13.4.3 we presented a very high-level summary of the Java and C++ memory models. Learn their details. Also investigate the (more loosely specified) models of Ada and C#. How do these compare? How efficiently can each be implemented on various real machines? What are the challenges for implementors? For Java, explore the controversy that arose around the memory model in the original definition of the language (updated in Java 5—see the paper by Manson et al. [MPA05] for a discussion). For C++, pay particular attention to the ability to specify weakened consistency on loads and stores of atomic variables.
13.43 在13.3.2 节中,我们简要介绍了非阻塞并发数据结构的设计,这种结构无需锁即可正常工作。了解有关此主题的更多信息。编写正确的非阻塞代码有多难?与基于锁的代码相比,其性能如何?您可能希望从 Michael [ MS98 ] 和 Sundell [ Sun04 ] 的工作开始。要获得更理论的基础,请从 Herlihy 关于等待自由的原始文章[ Her91 ] 和较新的阻塞自由概念[ HLM03 ]开始,或者查看 Herlihy 和 Shavit [ HS12 ] 的文本。
13.43 In Section 13.3.2 we presented a brief introduction to the design of nonblocking concurrent data structures, which work correctly without locks. Learn more about this topic. How hard is it to write correct nonblocking code? How does the performance compare to that of lock-based code? You might want to start with the work of Michael [MS98] and Sundell [Sun04]. For a more theoretical foundation, start with Herlihy's original article on wait freedom [Her91] and the more recent concept of obstruction freedom [HLM03], or check out the text by Herlihy and Shavit [HS12].
13.44 作为对读写锁的可能改进,了解序列锁[ Lam05 ] 和RCU(读取-复制更新)同步习惯用法 [ MAK + 01 ]。这两者都在操作系统社区中被广泛使用。讨论将它们应用于“非专家”编写的代码所涉及的挑战。
13.44 As possible improvements to reader-writer locks, learn about sequence locks [Lam05] and the RCU (read-copy update) synchronization idiom [MAK+01]. Both of these are heavily used in the operating systems community. Discuss the challenges involved in applying them to code written by “nonexperts.”
13.45 第一个软件事务内存系统源于对非阻塞并发数据结构的研究,并且实际上是非阻塞的。然而,大多数最新系统都是基于锁的。请阅读 Ennals [ Enn06 ] 的立场文件以及 Marathe 和 Moir [ MM08 ] 和 Tabba 等人 [ TWGM07 ]的最新论文。您怎么看?TM 系统应该是非阻塞的吗?
13.45 The first software transactional memory systems grew out of work on nonblocking concurrent data structures, and were in fact nonblocking. Most recent systems, however, are lock based. Read the position paper by Ennals [Enn06] and the more recent papers of Marathe and Moir [MM08] and Tabba et al. [TWGM07]. What do you think? Should TM systems be nonblocking?
13.46 最广泛使用的语言级事务内存是Haskell 的STM monad,由 Glasgow Haskell 编译器和运行时系统支持。阅读其语法和实现 [ HMPH05 ]。付费特别关注重试和orElse机制。讨论它们与条件临界区的相似之处和优势。
13.46 The most widely used language-level transactional memory is the STM monad of Haskell, supported by the Glasgow Haskell compiler and runtime system. Read up on its syntax and implementation [HMPH05]. Pay particular attention to the retry and orElse mechanisms. Discuss their similarities to—and advantages over—conditional critical regions.
13.47 研究一些您喜欢的库包的文档(可能是 C 和 C++ 标准库,或者 .NET 和 Java 库,或者许多可用的数学计算包)。哪些例程可以安全地从多线程程序中调用?哪些不能?是什么导致了这种差异?为什么不让所有例程都是线程安全的?
13.47 Study the documentation for some of your favorite library packages (the C and C++ standard libraries, perhaps, or the .NET and Java libraries, or the many available packages for mathematical computing). Which routines can safely be called from a multithreaded program? Which cannot? What accounts for the difference? Why not make all routines thread safe?
13.48 详细研究几种并发语言。下载实现并使用它们编写几种不同类型的并行程序。(例如,你可以尝试康威生命游戏、德劳内三角剖分和高斯消元法;所有这些的描述都可以在网上轻松找到。)写一篇关于你经验的论文。哪些行之有效?哪些行不通?你可能考虑的语言包括 Ada、C#、Cilk、Erlang、Go、Haskell、Java、Modula-3、Occam、Rust、SR 和 Swift。所有这些的参考资料都可以在附录 A中找到。
13.48 Undertake a detailed study of several concurrent languages. Download implementations and use them to write parallel programs of several different sorts. (You might, for example, try Conway's Game of Life, Delaunay Triangulation, and Gaussian Elimination; descriptions of all of these can easily be found on the Web.) Write a paper about your experience. What worked well? What didn't? Languages you might consider include Ada, C#, Cilk, Erlang, Go, Haskell, Java, Modula-3, Occam, Rust, SR, and Swift. References for all of these can be found in Appendix A.
13.49 了解本章末尾参考文献中讨论的超级计算语言:Co-Array Fortran、Titanium 和 UPC;Chapel、Fortress 和 X10。这些语言彼此之间有何区别?与 MPI 和 OpenMP 相比如何?与不太注重“高端”计算的语言相比如何?
13.49 Learn about the supercomputing languages discussed in the Bibliographic Notes at the end of the chapter: Co-Array Fortran, Titanium, and UPC; and Chapel, Fortress, and X10. How do these compare to one another? To MPI and OpenMP? To languages with less of a focus on “high-end” computing?
13.50 本着上一个问题的精神,了解 SHMEM 库包,它最初由 Cray, Inc. 的 Robert Numrich 开发,现在标准化为 OpenSHMEM ( openshmem.org )。SHMEM 广泛用于大规模多处理器和集群上的并行编程。它被描述为共享内存和消息传递的结合。这种描述合理吗?在什么情况下,shmem程序有望胜过 MPI 或 OpenMP 中的解决方案?
13.50 In the spirit of the previous question, learn about the SHMEM library package, originally developed by Robert Numrich of Cray, Inc., and now standardized as OpenSHMEM (openshmem.org). SHMEM is widely used for parallel programming on both large-scale multiprocessors and clusters. It has been characterized as a cross between shared memory and message passing. Is this a fair characterization? Under what circumstances might a shmem program be expected to outperform solutions in MPI or OpenMP?
13.51 本章的大部分内容都致力于并行程序中竞争的管理。这项任务的复杂性提出了一个诱人的问题:是否有可能设计一种功能强大、广泛使用的并发编程语言,并且其中的程序本质上不存在竞争?对于(大部分)肯定的答案,有三种截然不同的看法,请参阅 Edward Lee [ Lee06 ] 的著作、Haskell 的各种并发方言 [ NA01、JGF96 ] 和确定性并行 Java (DPJ) [ BAD + 09 ]。
13.51 Much of this chapter has been devoted to the management of races in parallel programs. The complexity of the task suggests a tantalizing question: is it possible to design a concurrent programming language that is powerful enough to be widely useful, and in which programs are inherently race-free? For three very different takes on a (mostly) affirmative answer, see the work of Edward Lee [Lee06], the various concurrent dialects of Haskell [NA01, JGF96], and Deterministic Parallel Java (DPJ) [BAD+09].
13.52–13.54 更深入。
13.52–13.54 In More Depth.
早期并发性研究主要源于 Dijkstra 的两篇文章 [ Dij68a、Dij72 ]。Andrews 和 Schneider [ AS83 ] 在 20 世纪 80 年代初对该领域进行了精彩的概述。Holt 等人的 [ HGLS78 ] 是许多并发性和同步性经典问题的有用参考。
Much of the early study of concurrency stems from a pair of articles by Dijkstra [Dij68a, Dij72]. Andrews and Schneider [AS83] provided an excellent snapshot of the field in the early 1980s. Holt et al. [HGLS78] is a useful reference for many of the classic problems in concurrency and synchronization.
彼得森双进程同步算法出现在一篇非常优雅且可读的两页论文中 [ Pet81 ]。Lamport 于 1978 年发表的有关“分布式系统中的时间、时钟和事件顺序”的文章 [ Lam78 ] 令人信服地指出,全局时间的概念无法得到很好的定义,因此分布式算法必须基于各个进程之间的因果关系。读写锁归功于 Courtois、Heymans 和 Parnas [ CHP71 ]。Java 7 phaser 部分受到 Shirako 等人的工作启发 [ SPSS08 ]。Mellor-Crummey 和 Scott [ MCS91 ] 调查了主要的忙等待同步算法,并引入了可在无争用的情况下扩展到大型机器的锁和屏障。
Peterson's two-process synchronization algorithm appears in a remarkably elegant and readable two-page paper [Pet81]. Lamport's 1978 article on “Time, Clocks, and the Ordering of Events in a Distributed System” [Lam78] argued convincingly that the notion of global time cannot be well defined, and that distributed algorithms must therefore be based on causal happens before relationships among individual processes. Reader–writer locks are due to Courtois, Heymans, and Parnas [CHP71]. Java 7 phasers were inspired in part by the work of Shirako et al. [SPSS08]. Mellor-Crummey and Scott [MCS91] survey the principal busy-wait synchronization algorithms and introduce locks and barriers that scale without contention to very large machines.
Herlihy [ Her91 ]撰写了有关无锁同步的开创性论文。Michael 和 Scott [ MS96 ]提出了示例 13.30中的非阻塞并发队列。Herlihy 和 Shavit [ HS12 ] 以及 Scott [ Sco13 ] 则提供了现代化的、一本书长度的有关同步和并发数据结构的介绍。Adve 和 Gharachorloo 引入了硬件内存模型的概念 [ AG96 ]。Pugh 解释了原始 Java 内存模型 [ Pug00 ]中存在的问题;Manson、Pugh 和 Adve [ MPA05 ] 描述了修订后的模型。Boehm 和 Adve [ BA08 ] 描述了 C++11 的内存模型。Boehm 令人信服地指出,如果没有编译器支持,线程就无法正确实现 [ Boe05 ]。有关事务内存的原始论文由 Herlihy 和 Moss [ HM93 ]撰写。 Harris、Larus 和 Rajwar 于 2010 年底对该领域进行了一本书长度的调查 [ HLR10 ]。Larus 和 Kozyrakis 提供了更简短的概述 [ LK08 ]。
The seminal paper on lock-free synchronization is that of Herlihy [Her91]. The nonblocking concurrent queue of Example 13.30 is due to Michael and Scott [MS96]. Herlihy and Shavit [HS12] and Scott [Sco13] provide modern, book-length coverage of synchronization and concurrent data structures. Adve and Gharachorloo introduce the notion of hardware memory models [AG96]. Pugh explains the problems with the original Java Memory Model [Pug00]; the revised model is described by Manson, Pugh, and Adve [MPA05]. The memory model for C++11 is described by Boehm and Adve [BA08]. Boehm has argued convincingly that threads cannot be implemented correctly without compiler support [Boe05]. The original paper on transactional memory is by Herlihy and Moss [HM93]. Harris, Larus, and Rajwar provide a book-length survey of the field as of late 2010 [HLR10]. Larus and Kozyrakis provide a briefer overview [LK08].
两代用于高端计算的并行语言影响深远。分区全局地址空间 (PGAS) 语言包括 Co-Array Fortran (CAF)、统一并行 C (UPC) 和 Titanium(Java 的一种方言)。它们支持变量的单一全局名称空间,但采用“额外维度”寻址来访问不在本地核心上的数据。CAF 的大部分功能已被 Fortran 2008 采用。所谓的 HPCS 语言(Chapel、Fortress 和 X10)以 PGAS 语言的经验为基础,但针对更广泛的硬件、应用程序和并行风格。这三种语言都包含事务功能。对于所有这些语言,网络搜索可能是当前信息的最佳来源。
Two recent generations of parallel languages for high-end computing have been highly influential. The Partitioned Global Address Space (PGAS) languages include Co-Array Fortran (CAF), Unified Parallel C (UPC), and Titanium (a dialect of Java). They support a single global name space for variables, but employ an “extra dimension” of addressing to access data not on the local core. Much of the functionality of CAF has been adopted into Fortran 2008. The so-called HPCS languages—Chapel, Fortress, and X10—build on experience with the PGAS languages, but target a broader range of hardware, applications, and styles of parallelism. All three include transactional features. For all of these, a web search is probably the best source of current information.
MPI [ Mes12 ] 已在各种文章和书籍中有所介绍。最新版本从早期的竞争系统 PVM(并行虚拟机)[ Sun90,GBD + 94 ] 中汲取了多项功能。在 Nelson 的博士研究 [ BN84 ] 之后,远程过程调用受到越来越多的关注。开放网络计算 RPC 标准已在 Internet RFC 编号 1831 [ Sri95 ]中有所介绍。RPC 还构成了 CORBA、COM、JavaBeans 和 SOAP 等高级标准的基础。
MPI [Mes12] is documented in a variety of articles and books. The latest version draws several features from an earlier, competing system known as PVM (Parallel Virtual Machine) [Sun90, GBD+94]. Remote procedure call received increasing attention in the wake of Nelson's doctoral research [BN84]. The Open Network Computing RPC standard is documented in Internet RFC number 1831 [Sri95]. RPC also forms the basis of such higher-level standards as CORBA, COM, JavaBeans, and SOAP.
软件分布式共享内存(S-DSM)最初由李在其博士论文中提出 [ LH89 ]。莱斯大学的 TreadMarks 系统被广泛认为是各种实现中最成熟、最强大的 [ ACD + 96 ]。
Software distributed shared memory (S-DSM) was originally proposed by Li as part of his doctoral research [LH89]. The TreadMarks system from Rice University was widely considered the most mature and robust of the various implementations [ACD+96].
传统编程语言主要用于构建独立的应用程序:接受某种输入、以某种易于理解的方式对其进行操作并生成适当输出的程序。但计算机的大多数实际用途都需要多个程序的协调。例如,大型机构工资系统必须处理来自读卡器、扫描纸质表格和手动(键盘)输入的时间报告数据;执行数千个数据库查询;执行数百条法律和机构规则;为记录保存、审计和纳税申报目的创建广泛的“纸质记录”;打印薪水支票;并与世界各地的服务器通信以进行在线直接存款、税收预扣、退休金积累、医疗保险等。这些任务可能涉及数十或数百个单独可执行的程序。这些程序之间的协调肯定需要测试和条件、循环、变量和类型、子例程和抽象——传统语言在应用程序内提供的逻辑工具相同。
Traditional programming languages are intended primarily for the construction of self-contained applications: programs that accept some sort of input, manipulate it in some well-understood way, and generate appropriate output. But most actual uses of computers require the coordination of multiple programs. A large institutional payroll system, for example, must process time-reporting data from card readers, scanned paper forms, and manual (keyboard) entry; execute thousands of database queries; enforce hundreds of legal and institutional rules; create an extensive “paper trail” for record-keeping, auditing, and tax preparation purposes; print paychecks; and communicate with servers around the world for on-line direct deposit, tax withholding, retirement accumulation, medical insurance, and so on. These tasks are likely to involve dozens or hundreds of separately executable programs. Coordination among these programs is certain to require tests and conditionals, loops, variables and types, subroutines and abstractions—the same sorts of logical tools that a conventional language provides inside an application.
在更小的范围内,平面设计师或摄影记者可能会定期从数码相机下载图片;将它们转换为喜欢的格式;旋转垂直拍摄的图片;对它们进行下采样以创建可浏览的缩略图版本;按日期、主题和颜色直方图对它们进行索引;将它们备份到远程存档;然后重新初始化相机的内存。手动执行这些步骤可能既繁琐又容易出错。同样,创建动态网页可能需要身份验证和授权、数据库查找、图像处理、远程通信以及 HTML 文本的读写。所有这些场景都表明需要协调其他程序的程序。
On a much smaller scale, a graphic artist or photojournalist may routinely download pictures from a digital camera; convert them to a favorite format; rotate the pictures that were shot in vertical orientation; down-sample them to create browsable thumbnail versions; index them by date, subject, and color histogram; back them up to a remote archive; and then reinitialize the camera's memory. Performing these steps by hand is likely to be both tedious and error-prone. In a similar vein, the creation of a dynamic web page may require authentication and authorization, database lookup, image manipulation, remote communication, and the reading and writing of HTML text. All these scenarios suggest a need for programs that coordinate other programs.
当然,用 Java、C 或其他传统语言编写协调代码是可能的,但这并不总是那么容易。传统语言往往强调效率、可维护性、可移植性和静态错误检测。它们的类型系统往往围绕硬件级概念构建,如固定大小的整数、浮点数、字符和数组。相比之下,脚本语言往往强调灵活性、快速开发、本地定制和动态(运行时)检查。同样,它们的类型系统倾向于包含诸如表、模式、列表和文件之类的高级概念。
It is of course possible to write coordination code in Java, C, or some other conventional language, but it isn't always easy. Conventional languages tend to stress efficiency, maintainability, portability, and the static detection of errors. Their type systems tend to be built around such hardware-level concepts as fixed-size integers, floating-point numbers, characters, and arrays. By contrast scripting languages tend to stress flexibility, rapid development, local customization, and dynamic (run-time) checking. Their type systems, likewise, tend to embrace such high-level concepts as tables, patterns, lists, and files.
通用脚本语言(例如 Perl、Python 和 Ruby)有时被称为粘合语言,因为它们最初设计用于将现有程序“粘合”在一起以构建更大的系统。随着万维网的发展,脚本语言已成为在服务器和客户端浏览器上生成动态内容的标准方式。它们还被广泛用于定制或扩展编辑器、电子表格、游戏和演示工具等“可编写脚本”系统的功能。
General-purpose scripting languages like Perl, Python, and Ruby are sometimes called glue languages, because they were originally designed to “glue” existing programs together to build a larger system. With the growth of the World Wide Web, scripting languages have become the standard way to generate dynamic content, both on servers and with the client browser. They are also widely used to customize or extend the functionality of such “scriptable” systems as editors, spreadsheets, games, and presentation tools.
我们将在14.1 节中更详细地讨论脚本的历史和性质。然后,我们将在14.2 节中讨论脚本广泛使用的一些问题领域。这些包括命令解释 (shell)、文本处理和报告生成、数学和统计、通用程序协调以及配置和扩展。在14.3 节中,我们将讨论万维网上使用的几种脚本形式,包括 CGI 脚本、嵌入在网页中的脚本的服务器端和客户端处理、Java 小程序以及(在配套站点上)XSLT。最后,在14.4 节中,我们将讨论许多脚本语言共有的一些更有趣的语言特性,这些特性使它们有别于更传统的“主流”同类语言。我们将特别讨论命名、作用域和类型;字符串和模式操作;以及高级结构化数据。我们不会详细介绍任何一种脚本语言,但会考虑几种脚本语言的具体示例。与本书的大部分内容一样,我们将重点介绍底层概念。
We consider the history and nature of scripting in more detail in Section 14.1. We then turn in Section 14.2 to some of the problem domains in which scripting is widely used. These include command interpretation (shells), text processing and report generation, mathematics and statistics, general-purpose program coordination, and configuration and extension. In Section 14.3 we consider several forms of scripting used on the World Wide Web, including CGI scripts, server- and client-side processing of scripts embedded in web pages, Java applets, and (on the companion site) XSLT. Finally, in Section 14.4, we consider some of the more interesting language features, common to many scripting languages, that distinguish them from their more traditional “mainstream” cousins. We look in particular at naming, scoping, and typing; string and pattern manipulation; and high-level structured data. We will not provide a detailed introduction to any one scripting language, though we will consider concrete examples in several. As in most of this book, the emphasis will be on underlying concepts.
现代脚本语言有两大祖先。第一组是传统批处理和“终端”(命令行)计算的命令解释器或“shell”。另一组是用于文本处理和报告生成的各种工具。第一组的例子包括 IBM 的 JCL、MS-DOS命令解释器以及 Unix sh和csh shell 系列。第二组的例子包括 IBM 的 RPG 以及 Unix 的sed和awk。从这些语言发展而来的是 Rexx(IBM 的“重组扩展执行器”,可追溯到 1979 年)和 Perl(最初由 Larry Wall 在 1980 年代后期设计,至今仍然是使用最广泛的通用脚本语言之一)。其他通用脚本语言包括 Python、Ruby、PowerShell(适用于 Windows)和 AppleScript(适用于 Mac)。
Modern scripting languages have two principal sets of ancestors. In one set are the command interpreters or “shells” of traditional batch and “terminal” (command-line) computing. In the other set are various tools for text processing and report generation. Examples in the first set include IBM's JCL, the MS-DOS command interpreter, and the Unix sh and csh shell families. Examples in the second set include IBM's RPG and Unix's sed and awk. From these evolved Rexx, IBM's “Restructured Extended Executor,” which dates from 1979, and Perl, originally devised by Larry Wall in the late 1980s, and still one of the most widely used general-purpose scripting languages. Other general-purpose scripting languages include Python, Ruby, PowerShell (for Windows), and AppleScript (for the Mac).
随着 20 世纪 90 年代末万维网的发展,Perl 被广泛用于“服务器端”Web 脚本,即 Web 服务器执行程序(在服务器的机器上)来生成页面内容。早期的 Web 脚本爱好者 Rasmus Lerdorf 就是其中之一,他创建了一组脚本来跟踪对其个人主页的访问。这些脚本最初是用 Perl 编写的,但很快被重新设计为一种成熟且独立的语言,并演变为 PHP,现在它是服务器端 Web 脚本最流行的平台。PHP 的竞争对手包括 JSP(Java Server Pages)、Ruby on Rails,以及 Microsoft 平台上的对于在客户端计算机上编写脚本,所有主流浏览器都实现了 JavaScript,这是 Netscape 公司在 20 世纪 90 年代中期开发的一种语言,并于 1999 年由 ECMA(欧洲标准机构)进行了标准化 [ ECM11 ]。
With the growth of the World Wide Web in the late 1990s, Perl was widely adopted for “server-side” web scripting, in which a web server executes a program (on the server's machine) to generate the content of a page. One early web-scripting enthusiast was Rasmus Lerdorf, who created a collection of scripts to track access to his personal home page. Originally written in Perl but soon redesigned as a full-fledged and independent language, these scripts evolved into PHP, now the most popular platform for server-side web scripting. PHP competitors include JSP (Java Server Pages), Ruby on Rails, and, on Microsoft platforms, PowerShell. For scripting on the client computer, all major browsers implement JavaScript, a language developed by Netscape Corporation in the mid 1990s, and standardized by ECMA (the European standards body) in 1999 [ECM11].
在有关脚本的经典论文 [ Ous98 ] 中,Tcl 的创建者 John Ousterhout 提出:“脚本语言假设其他语言中已经存在一组有用的组件。它们的目的不是用于从头编写应用程序,而是用于组合组件。”Ousterhout 设想了这样一个未来:程序员将越来越依赖脚本语言来构建系统的顶层结构,而清晰度、可重用性和易于开发性是其中的关键。他认为,C、C++ 或 Java 等传统“系统语言”将用于自包含、可重用的系统组件,这些组件强调复杂的算法或执行速度。他认为,这是一个至今看来仍然合理的一般经验法则,即使用脚本语言开发代码的速度可以提高 5 到 10 倍,但使用传统系统语言运行代码的速度可以提高 10 到 20 倍。
In a classic paper on scripting [Ous98], John Ousterhout, the creator of Tcl, suggested that “Scripting languages assume that a collection of useful components already exist in other languages. They are intended not for writing applications from scratch but rather for combining components.” Ousterhout envisioned a future in which programmers would increasingly rely on scripting languages for the top-level structure of their systems, where clarity, reusability, and ease of development are crucial. Traditional “systems languages” like C, C++, or Java, he argued, would be used for self-contained, reusable system components, which emphasize complex algorithms or execution speed. As a general rule of thumb that still seems reasonable today, he suggested that code could be developed 5 to 10 times faster in a scripting language, but would run 10 to 20 times faster in a traditional systems language.
一些作者将术语“脚本”保留用于协调多个程序的粘合语言。但在常见用法中,脚本是一个更广泛和更模糊的概念,不仅包含 Web 脚本,还包括扩展语言。这些通常嵌入在某些较大的宿主程序中,然后可以控制这些程序。许多读者都熟悉 Microsoft Office 和相关应用程序的 Visual Basic“宏”。其他人可能熟悉emacs文本编辑器的基于 Lisp 的扩展语言,或计算机游戏行业中广泛使用的 Lua。其他几种语言,包括 Tcl、Rexx、Python 以及 Scheme 的 Guile 和 Elk 方言,都有旨在嵌入到其他应用程序中的实现。同样,一些广泛使用的商业应用程序也提供了自己的专有扩展语言。对于图形用户界面 (GUI) 编程,最初设计用于 Tcl 的 Tk 工具包已被整合到几种脚本语言中,包括 Perl、Python 和 Ruby。
Some authors reserve the term “scripting” for the glue languages used to coordinate multiple programs. In common usage, however, scripting is a broader and vaguer concept, encompassing not only web scripting but also extension languages. These are typically embedded within some larger host program, which they can then control. Many readers will be familiar with the Visual Basic “macros” of Microsoft Office and related applications. Others may be familiar with the Lisp-based extension language of the emacs text editor, or the widespread use of Lua in the computer gaming industry. Several other languages, including Tcl, Rexx, Python, and the Guile and Elk dialects of Scheme, have implementations designed to be embedded in other applications. In a similar vein, several widely used commercial applications provide their own proprietary extension languages. For graphical user interface (GUI) programming, the Tk toolkit, originally designed for use with Tcl, has been incorporated into several scripting languages, including Perl, Python, and Ruby.
你也可以将 XSLT(可扩展样式表语言转换)视为一种脚本语言,尽管它与本章中讨论的其他语言略有不同。XSLT 是日益壮大的 XML(可扩展标记语言)工具家族的一部分。我们将在第14.3.5 节中进一步讨论它。
One can also view XSLT (extensible stylesheet language transformations) as a scripting language, albeit somewhat different from the others considered in this chapter. XSLT is part of the growing family of XML (extensible markup language) tools. We consider it further in Section 14.3.5.
虽然很难准确定义脚本语言,但它们往往具有几个共同的特征:
While it is difficult to define scripting languages precisely, there are several characteristics that they tend to have in common:
批处理和交互式都使用。一些脚本语言(尤其是 Perl)有一个编译器,它坚持在产生任何输出之前读取整个源程序。然而,大多数其他语言都愿意逐行编译或解释其输入。Rexx、Python、Tcl、Guile 以及(带有简短的帮助脚本)Ruby 和 Lua 都接受来自键盘的命令。
Both batch and interactive use. A few scripting languages (notably Perl) have a compiler that insists on reading the entire source program before it produces any output. Most other languages, however, are willing to compile or interpret their input line by line. Rexx, Python, Tcl, Guile, and (with short helper scripts) Ruby and Lua will all accept commands from the keyboard.
表达经济。为了支持快速开发和交互式使用,脚本语言往往需要最少的“样板”。有些脚本语言大量使用标点符号和非常短的标识符(Perl 因这一点而臭名昭著),而其他脚本语言(例如 Rexx、Tcl 和 AppleScript)则更倾向于“英语化”,单词很多但标点符号很少。所有脚本语言都试图避免传统语言中常见的大量声明和顶级结构。
Economy of expression. To support both rapid development and interactive use, scripting languages tend to require a minimum of “boilerplate.” Some make heavy use of punctuation and very short identifiers (Perl is notorious for this), while others (e.g., Rexx, Tcl, and AppleScript) tend to be more “English-like,” with lots of words and not much punctuation. All attempt to avoid the extensive declarations and top-level structure common to conventional languages.
缺少声明;作用域规则简单。大多数脚本语言都省去了声明,而是提供了简单的规则来管理名称的作用域。在某些语言(例如 Perl)中,默认情况下所有内容都是全局的;可以使用可选声明将变量限制在嵌套作用域内。在其他语言(例如 PHP 和 Tcl)中,默认情况下所有内容都是本地的;全局变量必须显式导入。Python 采用了一个有趣的规则,即任何被赋值的变量都是赋值出现所在的块的本地变量。需要使用特殊语法才能在周围作用域中赋值给变量。
Lack of declarations; simple scoping rules. Most scripting languages dispense with declarations, and provide simple rules to govern the scope of names. In some languages (e.g., Perl) everything is global by default; optional declarations can be used to limit a variable to a nested scope. In other languages (e.g., PHP and Tcl), everything is local by default; globals must be explicitly imported. Python adopts the interesting rule that any variable that is assigned a value is local to the block in which the assignment appears. Special syntax is required to assign to a variable in a surrounding scope.
轻松访问系统设施。大多数编程语言都提供了一种方法,可以要求底层操作系统运行另一个程序,或直接执行某些操作。然而,在脚本语言中,这些请求更为基础,并且有更直接的支持。例如,Perl 提供了 100 多个内置命令,这些命令可以访问操作系统函数,用于输入和输出、文件和目录操作、进程管理、数据库访问、套接字、进程间通信和同步、保护和授权、时钟和网络通信。这些内置命令通常比 C 等语言中相应的库调用更容易使用。
Easy access to system facilities. Most programming languages provide a way to ask the underlying operating system to run another program, or to perform some operation directly. In scripting languages, however, these requests are much more fundamental, and have much more direct support. Perl, for one, provides well over 100 built-in commands that access operating system functions for input and output, file and directory manipulation, process management, database access, sockets, interprocess communication and synchronization, protection and authorization, time-of-day clock, and network communication. These built-in commands are generally a good bit easier to use than corresponding library calls in languages like C.
复杂的模式匹配和字符串操作。为了保持其文本处理和报告生成血统,并方便外部程序处理文本输入和输出,脚本语言往往具有极其丰富的模式匹配、搜索和字符串操作功能。通常,这些功能基于扩展的正则表达式。我们将在第 14.4.2 节中进一步讨论它们。
Sophisticated pattern matching and string manipulation. In keeping with their text processing and report generation ancestry, and to facilitate the manipulation of textual input and output for external programs, scripting languages tend to have extraordinarily rich facilities for pattern matching, search, and string manipulation. Typically these are based on extended regular expressions. We discuss them further in Section 14.4.2.
高级数据类型。集合、包、字典、列表和元组等高级数据类型在传统编程语言的标准库包中越来越常见。一些语言(尤其是 C++)允许用户重新定义标准中缀运算符,使这些类型像更原始的以硬件为中心的类型一样易于使用。脚本语言更进一步,将高级类型构建到语言本身的语法和语义中。例如,在大多数脚本语言中,通常有一个由字符串索引的“数组”,其底层实现基于哈希表。存储总是会被垃圾收集。
High-level data types. High-level data types like sets, bags, dictionaries, lists, and tuples are increasingly common in the standard library packages of conventional programming languages. A few languages (notably C++) allow users to redefine standard infix operators to make these types as easy to use as more primitive, hardware-centric types. Scripting languages go one step further by building high-level types into the syntax and semantics of the language itself. In most scripting languages, for example, it is commonplace to have an “array” that is indexed by character strings, with an underlying implementation based on hash tables. Storage is invariably garbage collected.
当今编程语言中变化最快的大部分是脚本语言。这可以归因于多种原因,包括 Web 的持续增长、开源社区的活力以及创建新脚本语言所需的投资相对较低。Java 或 C# 等编译型工业级语言需要非常庞大的编程团队投入多年时间才能开发出来,而一位才华横溢的设计师独自工作,仅需一两年时间就能开发出可用的新脚本语言实现。
Much of the most rapid change in programming languages today is occurring in scripting languages. This can be attributed to several causes, including the continued growth of the Web, the dynamism of the open-source community, and the comparatively low investment required to create a new scripting language. Where a compiled, industrial-quality language like Java or C# requires a multiyear investment by a very large programming team, a single talented designer, working alone, can create a usable implementation of a new scripting language in only a year or two.
部分由于这种快速变化,较新的脚本语言已经能够融入语言设计中的一些最具创新性的概念。例如,Ruby 具有统一的对象模型(非常类似于 Smalltalk)、真正的迭代器(如 Clu)、lambda 表达式(如 Lisp)、数组切片(如 Fortran 90)、结构化异常处理、多路赋值和反射。Python 也具有许多这些功能,以及 Ruby 所缺乏的一些功能,包括 Haskell 风格的列表推导。
Due in part to this rapid change, newer scripting languages have been able to incorporate some of the most innovative concepts in language design. Ruby, for example, has a uniform object model (much like Smalltalk), true iterators (like Clu), lambda expressions, (like Lisp), array slices (like Fortran 90), structured exception handling, multiway assignment, and reflection. Python has many of these features as well, and a few that Ruby lacks, including Haskell-style list comprehensions.
一些通用语言(例如 Scheme 和 Visual Basic)被广泛用于脚本编写。相反,一些脚本语言(包括 Perl)Python 和 Ruby 的设计者旨在将其用于通用目的,其功能旨在支持“大规模编程”:模块、单独编译、反射、程序开发环境等。然而,在大多数情况下,脚本语言往往主要用在明确定义的问题领域。我们将在以下小节中讨论其中的一些。
Some general-purpose languages—Scheme and Visual Basic, for example—are widely used for scripting. Conversely, some scripting languages, including Perl, Python, and Ruby, are intended by their designers for general-purpose use, with features intended to support “programming in the large”: modules, separate compilation, reflection, program development environments, and so on. For the most part, however, scripting languages tend to see their principal use in well-defined problem domains. We consider some of these in the following subsections.
在穿孔卡片计算机时代(大概到 20 世纪 70 年代中期),简单的命令语言允许用户“编写”一副卡片的处理程序。例如,卡片组前面的控制卡可能表示即将出现的卡片代表要编译的程序,或者可能是编译器本身的机器语言,或者是已编译并存储在磁盘上的程序的输入。卡片组后面嵌入的控制卡可能会测试最近执行的程序的退出状态,并根据该程序是否成功完成来选择下一步要做什么。然而,鉴于卡片组的线性特性(通常无法备份),批处理的命令语言往往不太复杂。例如,JCL 没有迭代结构。
In the days of punch-card computing (through perhaps the mid 1970s), simple command languages allowed the user to “script” the processing of a card deck. A control card at the front of the deck, for example, might indicate that the upcoming cards represented a program to be compiled, or perhaps machine language for the compiler itself, or input for a program already compiled and stored on disk. A control card embedded later in the deck might test the exit status of the most recently executed program and choose what to do next based on whether that program completed successfully. Given the linear nature of a card deck, however (one can't in general back up), command languages for batch processing tended not to be very sophisticated. JCL, for example, had no iteration constructs.
随着 20 世纪 60 年代和 70 年代早期交互式分时技术的发展,命令语言变得更加复杂。1963 年和 1964 年,Louis Pouzin 为 MIT 的兼容分时系统 (CTSS) 编写了一个简单的命令解释器。1964 年,当开创性的 Multics 系统开始投入工作时,Pouzin 绘制了一种扩展命令语言的设计草图,该语言具有引用和参数传递机制,他为此创造了术语“shell”。随后的实现为 Ken Thompson 在 1973 年设计原始 Unix shell 提供了灵感。20 世纪 70 年代中期,Stephen Bourne 和 John Mashey 分别用控制流和变量扩展了 Thompson shell;Bourne 的设计被采纳为 Unix 标准,取代了 Thompson shell(并命名为 sh)。
With the development of interactive timesharing in the 1960s and early 1970s, command languages became much more sophisticated. Louis Pouzin wrote a simple command interpreter for CTSS, the Compatible Time Sharing System at MIT, in 1963 and 1964. When work began on the groundbreaking Multics system in 1964, Pouzin sketched the design of an extended command language, with quoting and argument-passing mechanisms, for which he coined the term “shell.” The subsequent implementation served as inspiration for Ken Thompson in the design of the original Unix shell in 1973. In the mid-1970s, Stephen Bourne and John Mashey separately extended the Thompson shell with control flow and variables; Bourne's design was adopted as the Unix standard, taking the place (and the name) of the Thompson shell, sh.
20 世纪 70 年代末,Bill Joy 开发了所谓的“C shell”(csh),其灵感至少部分来源于 Mashey 的语法,并引入了显著的增强功能以供交互使用,包括历史记录、别名和作业控制。csh的tcsh版本添加了命令行编辑和命令完成功能。 David Korn 将这些机制融入了 Bourne shell 的直系后代ksh中,该 shell 非常类似于标准 POSIX shell [ Int03b ]。流行的“Bourne-again”shell bash是ksh的开源版本。虽然tcsh在某些领域仍然很流行,但ksh/bash /POSIX sh在编写 shell 脚本方面要好得多,在交互使用方面也堪称一流。
In the late 1970s Bill Joy developed the so-called “C shell” (csh), inspired at least in part by Mashey's syntax, and introducing significant enhancements for interactive use, including history, aliases, and job control. The tcsh version of csh adds command-line editing and command completion. David Korn incorporated these mechanisms into a direct descendant of the Bourne shell, ksh, which is very similar to the standard POSIX shell [Int03b]. The popular “Bourne-again” shell, bash, is an open-source version of ksh. While tcsh is still popular in some quarters, ksh/bash/POSIX sh is substantially better for writing shell scripts, and comparable for interactive use.
除了专为交互使用而设计的功能(我们在此不再赘述)之外,shell 语言还提供了大量机制来操作文件名、参数和命令,以及将其他程序粘合在一起。大多数这些功能都保留在更通用的脚本语言中。我们在此使用bash语法来考虑其中的一些功能。讨论必然会大大简化;完整详细信息可在bash 手册页或各种在线教程中找到。
In addition to features designed for interactive use, which we will not consider further here, shell languages provide a wealth of mechanisms to manipulate filenames, arguments, and commands, and to glue together other programs. Most of these features are retained by more general scripting languages. We consider a few of them here, using bash syntax. The discussion is of necessity heavily simplified; full details can be found in the bash man page, or in various on-line tutorials.
在示例 14.6中,我们使用方括号括起条件表达式。双方括号具有类似的用途,但表达式语法更像 C,并且没有文件名扩展。双括号用于括起算术计算,同样具有类似 C 的语法。
In Example 14.6 we used square brackets to enclose a conditional expression. Double square brackets serve a similar purpose, but with more C-like expression syntax, and without filename expansion. Double parentheses are used to enclose arithmetic computations, again with C-like syntax.
在$()或反引号中插入命令、在{}中插入模式以及在(())中插入算术表达式都被视为扩展形式,类似于文件名扩展和变量扩展。将字符串拆分为单词也被视为一种扩展形式,在某些情况下,用用户主目录的名称替换波浪符号 ( ~ ) 也被视为一种扩展形式。总而言之,这些为我们提供了Bash 中的七种不同类型的扩展。
The interpolation of commands in $() or backquotes, patterns in {}, and arithmetic expressions in (()) are all considered forms of expansion, analogous to filename expansion and variable expansion. The splitting of strings into words is also considered a form of expansion, as is the replacement, in certain contexts, of tilde (~) characters with the name of the user's home directory. All told, these give us seven different kinds of expansion in bash.
所有各种括号结构都有规则来规定在其中执行哪些类型的扩展。这些规则旨在尽可能直观,但它们在各个结构中并不统一。例如,文件名扩展不会在[[ ]]括号条件中发生。类似地,如果使用反斜杠转义,双引号字符可能会出现在双引号字符串内,但单引号字符可能不会出现在单引号字符串内。
All of the various bracketing constructs have rules governing which kinds of expansion are performed within. The rules are intended to be as intuitive as possible, but they are not uniform across constructs. Filename expansion, for example, does not occur within [[ ]] -bracketed conditions. Similarly, a double-quote character may appear inside a double-quoted string if escaped with a backslash, but a single-quote character may not appear inside a single-quoted string.
Shell 语言往往高度面向字符串。命令是字符串,解析为单词列表。变量是字符串值。变量扩展机制允许用户提取前缀、后缀或任意子字符串。连接通过简单的并置表示。有复杂的引用约定。很少有更传统的语言对字符串有类似的支持。
Shell languages tend to be heavily string-oriented. Commands are strings, parsed into lists of words. Variables are string-valued. Variable expansion mechanisms allow the user to extract prefixes, suffixes, or arbitrary substrings. Concatenation is indicated by simple juxtaposition. There are elaborate quoting conventions. Few more conventional languages have similar support for strings.
同时,shell 语言显然不适用于emacs或vim 等编辑器中常见的文本操作。特别是,shell 语言缺少搜索和替换功能,而且编辑器通过一次击键即可完成的许多其他任务(插入、删除、替换、括号匹配、向前和向后移动)在 shell 环境中很难实现,或者根本没有意义。对于重复的文本操作,人们自然希望使编辑过程自动化。完成此任务的工具构成了现代脚本语言的第二大祖先类。
At the same time, shell languages are clearly not intended for the sort of text manipulation commonly performed in editors like emacs or vim. Search and substitution, in particular, are missing, and many other tasks that editors accomplish with a single keystroke—insertion, deletion, replacement, bracket matching, forward and backward motion—would be awkward to implement, or simply make no sense, in the context of the shell. For repetitive text manipulation it is natural to want to automate the editing process. Tools to accomplish this task constitute the second principal class of ancestors for modern scripting languages.
为了解决sed的局限性,Alfred Aho、Peter Weinberger 和 Brian Kernighan 于 1977 年设计了awk(该名称取自他们姓氏的首字母)。从某种意义上说,Awk是sed等流编辑器与成熟脚本语言之间的进化纽带。它保留了sed的一次一行过滤计算模型,但允许用户在需要时退出该模型,并使用类似于 C 的语法替换单字符编辑命令。Awk提供(无类型)变量和各种控制流构造,包括子例程。
In an attempt to address the limitations of sed, Alfred Aho, Peter Weinberger, and Brian Kernighan designed awk in 1977 (the name is based on the initial letters of their last names). Awk is in some sense an evolutionary link between stream editors like sed and full-fledged scripting languages. It retains sed's line-at-a-time filter model of computation, but allows the user to escape this model when desired, and replaces single-character editing commands with syntax reminiscent of C. Awk provides (typeless) variables and a variety of control-flow constructs, including subroutines.
Perl 最初由 Larry Wall 于 1987 年开发,当时他在美国国家安全局工作。最初的版本,大致是试图结合sed、awk和sh 的最佳功能。它是一个仅适用于 Unix 的工具,主要用于文本处理(名称代表“实用提取和报告语言”)。多年来,Perl 发展成为一种庞大而复杂的语言,拥有庞大的用户社区。多年来,它无疑是最流行和使用最广泛的脚本语言,尽管这一领先地位最近已被 Python、Ruby 和其他语言所取代。Perl 的速度足以满足许多通用用途,并且包括适合大型项目的单独编译、模块化和动态库机制。它已被移植到几乎所有已知的操作系统。
Perl was originally developed by Larry Wall in 1987, while he was working at the National Security Agency. The original version was, to first approximation, an attempt to combine the best features of sed, awk, and sh. It was a Unix-only tool, meant primarily for text processing (the name stands for “practical extraction and report language”). Over the years Perl grew into a large and complex language, with an enormous user community. For many years it was clearly the most popular and widely used scripting language, though that lead has more recently been lost to Python, Ruby, and others. Perl is fast enough for much general-purpose use, and includes separate compilation, modularization, and dynamic library mechanisms appropriate for large-scale projects. It has been ported to almost every known operating system.
Perl 的语言核心相对简单,但有大量内置库函数以及同样多的快捷方式和特殊情况。标准语言参考 [ CfWO12,第 722 页] 中可以找到这种丰富表达方式的提示,其中仅列出了 97 个“行为在不同平台之间差异最大的”内置函数。第三版的封面上印着这样一句话:“做事不止一种方法。”
Perl consists of a relatively simple language core, augmented with an enormous number of built-in library functions and an equally enormous number of shortcuts and special cases. A hint at this richness of expression can be found in the standard language reference [CfWO12, p. 722], which lists (only) the 97 built-in functions “whose behavior varies the most across platforms.” The cover of the third edition was emblazoned with the motto: “There's more than one way to do it.”
正如我们在讨论sed和awk 时所指出的,文本处理和报告生成的一个显著特征是频繁使用“单行程序”和其他简单脚本。任何曾经在电子表格单元格中输入过公式的人都知道,数学和统计学中也有类似的需求。正如 shell 和报告生成工具已经发展成为用于通用计算的强大语言一样,用于数学和统计计算的符号和工具也已经发展成为强大的语言。
As we noted in our discussions of sed and awk, one of the distinguishing characteristics of text processing and report generation is the frequent use of “one-line programs” and other simple scripts. Anyone who has ever entered formulas in the cells of a spreadsheet realizes that similar needs arise in mathematics and statistics. And just as shell and report generation tools have evolved into powerful languages for general-purpose computing, so too have notations and tools for mathematical and statistical computing.
在8.2.1 节(“切片和数组操作”)中,我们提到了 APL,这是 20 世纪 60 年代最不寻常的语言之一。APL 最初被认为是一种用于教授应用数学的纸笔符号,当它演变为一种编程语言时,它仍然强调数学算法的简洁、优雅的表达。虽然它既不能轻松访问其他程序,也不能进行复杂的字符串操作,但 APL 表现出了14.1.1 节中描述的所有其他脚本特征,有时人们会发现它被列为一种脚本语言。
In Section 8.2.1 (“Slices and Array Operations”), we mentioned APL, one of the more unusual languages of the 1960s. Originally conceived as a pen-and-paper notation for teaching applied mathematics, APL retained its emphasis on the concise, elegant expression of mathematical algorithms when it evolved into a programming language. Though it lacked both easy access to other programs and sophisticated string manipulation, APL displayed all the other characteristics of scripting described in Section 14.1.1, and one sometimes finds it listed as a scripting language.
APL 的现代继任者包括三个用于数学计算的商业软件包:Maple、Mathematica 和 Matlab。虽然它们的设计理念不同,但每个都为数值方法、符号数学(公式操作)、数据可视化和数学提供了广泛的支持建模。这三者都提供了强大的脚本语言,并且主要面向科学和工程应用。
The modern successors to APL include a trio of commercial packages for mathematical computing: Maple, Mathematica, and Matlab. Though their design philosophies differ, each provides extensive support for numerical methods, symbolic mathematics (formula manipulation), data visualization, and mathematical modeling. All three provide powerful scripting languages, with a heavy orientation toward scientific and engineering applications.
正如“3M”之于数学计算,S 和 R 语言之于统计计算。S 最初由 John Chambers 及其同事于 20 世纪 70 年代末在贝尔实验室开发,是一款商业软件包,广泛应用于统计学界和社会和行为科学的定量分支。R 是 S 的开源替代品,虽然不完全兼容,但在很大程度上兼容其商业版本。除其他功能外,R 还支持多维数组和列表类型、数组切片操作、用户定义的中缀运算符、按需调用参数、一流函数和无限范围。
As the “3 Ms” are to mathematical computing, so the S and R languages are to statistical computing. Originally developed at Bell Labs by John Chambers and colleagues in the late 1970s, S is a commercial package widely used in the statistics community and in quantitative branches of the social and behavioral sciences. R is an open-source alternative to S that is largely though not entirely compatible with its commercial cousin. Among other things, R supports multidimensional array and list types, array slice operations, user-defined infix operators, call-by-need parameters, first-class functions, and unlimited extent.
脚本语言从文本处理祖先那里继承了一套丰富的模式匹配和字符串操作机制。从命令解释器 shell 那里,它们继承了各种附加功能,包括简单的语法;灵活的类型;轻松创建和管理子程序,具有 I/O 重定向和访问完成状态;文件查询;轻松的交互式和基于文件的 I/O;轻松访问命令行参数、环境字符串、进程标识符、时钟等;以及自动启动解释器(# !约定)。如第 14.1.1 节所述,许多脚本语言都有可以交互接受命令的解释器。
From their text-processing ancestors, scripting languages inherit a rich set of pattern matching and string manipulation mechanisms. From command interpreter shells they inherit a wide variety of additional features including simple syntax; flexible typing; easy creation and management of subprograms, with I/O redirection and access to completion status; file queries; easy interactive and file-based I/O; easy access to command-line arguments, environment strings, process identifiers, time-of-day clock, and so on; and automatic interpreter start-up (the #! convention). As noted in Section 14.1.1, many scripting languages have interpreters that will accept commands interactively.
除了结合使用 shell 和文本处理机制之外,典型的胶水语言还提供了丰富的内置操作库,用于访问底层操作系统的功能,包括文件、目录和 I/O;进程和进程组;保护和授权;进程间通信和同步;定时和信号;以及套接字、名称服务和网络通信。正如文本处理机制最大限度地减少了使用sed、awk和grep等外部工具的需要一样,操作系统内置函数最大限度地减少了对其他外部工具的需求。
Beyond the combination of shell and text-processing mechanisms, the typical glue language provides an extensive library of built-in operations to access features of the underlying operating system, including files, directories, and I/O; processes and process groups; protection and authorization; interprocess communication and synchronization; timing and signals; and sockets, name service, and network communication. Just as text-processing mechanisms minimize the need to employ external tools like sed, awk, and grep, operating system builtins minimize the need for other external tools.
与此同时,随着时间的推移,脚本语言已经开发出一套丰富的内部计算功能。大多数脚本语言对数学的支持都比 shell 中的数学支持好得多。包括 Scheme、Python 和 Ruby 在内的几种脚本语言都支持任意精度算术。大多数脚本语言都为高级类型提供广泛的支持,包括数组、字符串、元组、列表和哈希(关联数组)。一些脚本语言支持类和面向对象。一些脚本语言支持迭代器、延续、线程、反射以及一等函数和高阶函数。包括 Perl、Python 和 Ruby 在内的一些脚本语言支持模块和动态加载,用于“大规模编程”。这些功能可以最大限度地增加脚本语言本身可以编写的代码量,并最大限度地减少使用更传统的编译语言的需要。
At the same time, scripting languages have, over time, developed a rich set of features for internal computation. Most have significantly better support for mathematics than is typically found in a shell. Several, including Scheme, Python, and Ruby, support arbitrary precision arithmetic. Most provide extensive support for higher-level types, including arrays, strings, tuples, lists, and hashes (associative arrays). Several support classes and object orientation. Some support iterators, continuations, threads, reflection, and first-class and higher-order functions. Some, including Perl, Python, and Ruby, support modules and dynamic loading, for “programming in the large.” These features serve to maximize the amount of code that can be written in the scripting language itself, and to minimize the need to escape to a more traditional, compiled language.
总而言之,通用脚本的理念是尽可能简单地构建程序的整体框架,仅在执行特殊用途任务时才使用外部工具,仅在性能至关重要时才使用编译语言。
In summary, the philosophy of general-purpose scripting is to make it as easy as possible to construct the overall framework of a program, escaping to external tools only for special-purpose tasks, and to compiled languages only when performance is at a premium.
如第 14.1 节所述,Rexx 被普遍认为是第一种通用脚本语言,比 Perl 和 Tcl 早了近十年。Perl 和 Tcl 大致同时代:它们都是在 20 世纪 80 年代末开发的。Perl 最初用于粘合和文本处理应用程序。Tcl 最初是一种扩展语言,但很快也发展成为粘合应用程序。随着脚本在 20 世纪 90 年代的流行,用户开始开发其他语言,提供更多功能,满足特定应用领域的需求(后面的部分将对此进行详细介绍),或者支持更符合其设计者个人品味的编程风格。
As noted in Section 14.1, Rexx is generally considered the first of the general-purpose scripting languages, predating Perl and Tcl by almost a decade. Perl and Tcl are roughly contemporaneous: both were initially developed in the late 1980s. Perl was originally intended for glue and text-processing applications. Tcl was originally an extension language, but soon grew into glue applications as well. As the popularity of scripting grew in the 1990s, users were motivated to develop additional languages, to provide additional features, address the needs of specific application domains (more on this in subsequent sections), or support a style of programming more in keeping with the personal taste of their designers.
Python 最初由 Guido van Rossum 于 20 世纪 90 年代初在荷兰阿姆斯特丹的 CWI 开发。他于 1995 年开始在弗吉尼亚州雷斯顿的 CNRI 继续工作。经过一系列后续变动后,他于 2005 年加入 Google。该语言的最新版本归 Python 软件基金会所有。所有版本都是开源的。
Python was originally developed by Guido van Rossum at CWI in Amsterdam, the Netherlands, in the early 1990s. He continued his work at CNRI in Reston, Virginia, beginning in 1995. After a series of subsequent moves, he joined Google in 2005. Recent versions of the language are owned by the Python Software Foundation. All releases are open source.
虽然我们的“强制退出”程序至少可以部分传达出 Perl 和 Python 的“感觉”,但它无法捕捉到它们功能的广度。Python 包含前面章节中讨论的许多更有趣的功能,包括具有静态作用域的嵌套函数、lambda 表达式和高阶函数、真正的迭代器、列表推导、数组切片操作、反射、结构化异常处理、多重继承以及模块和动态加载。其中许多也出现在 Ruby 中。
While our “force quit” program may convey, at least in part, the “feel” of Perl and Python, it cannot capture the breadth of their capabilities. Python includes many of the more interesting features discussed in earlier chapters, including nested functions with static scoping, lambda expressions and higher-order functions, true iterators, list comprehensions, array slice operations, reflection, structured exception handling, multiple inheritance, and modules and dynamic loading. Many of these also appear in Ruby.
Ruby 是 20 世纪 90 年代初由 Yukihiro “Matz” Matsumoto 在日本开发的。Matz 写道,他“想要一种比 Perl 更强大、比 Python 更面向对象的语言”[ TFH13,前言]。第一个公开版本于 1995 年发布,并迅速在日本广受欢迎。随着 2001 年英文文档 [ TFH13,第 1 版] 的发布,Ruby 也在其他地方迅速传播。它的成功很大程度上要归功于 Ruby on Rails Web 开发框架。Rails 最初由 David Heinemeier Hansson 于 2004 年发布,随后被几家主要公司采用,尤其是 Apple(它将其包含在 Mac OS 10.5“Leopard”版本中)和 Twitter(它在其基础架构的早期版本中使用了它)。
Ruby was developed in Japan in the early 1990s by Yukihiro “Matz” Matsumoto. Matz writes that he “wanted a language more powerful than Perl, and more object-oriented than Python” [TFH13, Foreword]. The first public release was made available in 1995, and quickly gained widespread popularity in Japan. With the publication in 2001 of English-language documentation [TFH13, 1st ed.], Ruby spread rapidly elsewhere as well. Much of its success can be credited to the Ruby on Rails web-development framework. Originally released by David Heinemeier Hansson in 2004, Rails was subsequently adopted by several major players—notably Apple, which included it in the 10.5 “Leopard” release of the Mac OS, and Twitter, which used it for early versions of their infrastructure.
大多数应用程序都会接受某种命令,命令告诉它们要做什么。有时这些命令以文本形式输入;更常见的是它们由用户界面事件触发,例如鼠标单击、菜单选择和按键。图形绘制程序中的命令可能会保存或加载图形;选择、插入、删除或修改其部分;选择线条样式、粗细或颜色;缩放或旋转显示;或修改用户首选项。
Most applications accept some sort of commands, which tell them what to do. Sometimes these commands are entered textually; more often they are triggered by user interface events such as mouse clicks, menu selections, and keystrokes. Commands in a graphical drawing program might save or load a drawing; select, insert, delete, or modify its parts; choose a line style, weight, or color; zoom or rotate the display; or modify user preferences.
扩展语言允许用户创建新命令,通常使用现有命令作为构建块,从而提高应用程序的实用性。扩展语言被广泛认为是复杂工具的基本功能。Adobe 的图形套件(Illustrator、Photoshop、InDesign 等)可以使用 JavaScript、Visual Basic(在 Windows 上)或 AppleScript(在 Mac 上)进行扩展(编写脚本)。迪士尼和工业光魔使用 Python 来扩展其内部(专有)工具。计算机游戏行业大量使用 Lua 来编写商业和开源游戏引擎的脚本。许多商用工具,包括 AutoCAD、Maya、Director 和 Flash,都有自己独特的脚本语言。这个列表只是冰山一角。
An extension language serves to increase the usefulness of an application by allowing the user to create new commands, generally using the existing commands as building blocks. Extension languages are widely regarded as an essential feature of sophisticated tools. Adobe's graphics suite (Illustrator, Photoshop, InDesign, etc.) can be extended (scripted) using JavaScript, Visual Basic (on Windows), or AppleScript (on the Mac). Disney and Industrial Light & Magic use Python to extend their internal (proprietary) tools. The computer gaming industry makes heavy use of Lua for scripting of both commercial and open-source game engines. Many commercially available tools, including AutoCAD, Maya, Director, and Flash, have their own unique scripting languages. This list barely scratches the surface.
为了实现扩展,工具必须
To admit extension, a tool must
■ incorporate, or communicate with, an interpreter for a scripting language.
■ 提供允许脚本调用该工具现有命令的挂钩。
■ provide hooks that allow scripts to call the tool's existing commands.
■ 允许用户将新定义的命令与用户界面事件联系起来。
■ allow the user to tie newly defined commands to user interface events.
只要小心谨慎,这些机制就可以独立于任何特定的脚本语言。微软的 Windows 脚本接口允许使用几乎任何语言来编写操作系统、Web 服务器和浏览器的脚本。GIMP 是广泛使用的 GNU 图像处理程序,具有类似的通用接口:它带有一个 Scheme 方言的内置解释器,并支持 Perl 和 Tcl 等插件(外部提供的解释器模块)。当然,用户社区倾向于使用一种最喜欢的语言,以促进代码共享。Microsoft 工具通常使用 PowerShell 编写脚本;GIMP 使用 Scheme;Adobe 工具在 PC 上用 Visual Basic 编写,在 Mac 上用 AppleScript 编写。
With care, these mechanisms can be made independent of any particular scripting language. Microsoft's Windows Script interface allows almost any language to be used to script the operating system, web server, and browser. GIMP, the widely used GNU Image Manipulation Program, has a comparably general interface: it comes with a built-in interpreter for a dialect of Scheme, and supports plug-ins (externally provided interpreter modules) for Perl and Tcl, among others. There is a tendency, of course, for user communities to converge on a favorite language, to facilitate sharing of code. Microsoft tools are usually scripted with PowerShell; GIMP with Scheme; Adobe tools with Visual Basic on the PC, or AppleScript on the Mac.
现存最古老的扩展机制之一是用于编写本书的emacs文本编辑器。为emacs创建了大量扩展包;其中许多都默认安装在标准发行版中。事实上,用户认为的编辑器核心功能中的大部分实际上是由扩展提供的;真正内置的部分相对较小。
One of the oldest existing extension mechanisms is that of the emacs text editor, used to write this book. An enormous number of extension packages have been created for emacs; many of them are installed by default in the standard distribution. In fact much of what users consider the editor's core functionality is actually provided by extensions; the truly built-in parts are comparatively small.
万维网上的大部分内容(尤其是搜索引擎可见的内容)都是静态的:页面很少甚至根本不会发生变化。但超文本(Web 所基于的抽象概念)始终被认为是一种表示“复杂、变化和不确定”的方式 [ Nel65 ]。当今 Web 的大部分功能在于它能够提供移动的页面、播放声音的页面、响应用户操作的页面,或者(也许最重要的是)响应页面获取请求,提供按需创建或格式化的信息。
Much of the content of the World Wide Web—particularly the content that is visible to search engines—is static: pages that seldom, if ever, change. But hypertext, the abstract notion on which the Web is based, was always conceived as a way to represent “the complex, the changing, and the indeterminate” [Nel65]. Much of the power of the Web today lies in its ability to deliver pages that move, play sounds, respond to user actions, or—perhaps most important—contain information created or formatted on demand, in response to the page-fetch request.
从编程语言的角度来看,简单地播放录制的音频或视频并不是特别有趣。因此,我们在这里将注意力集中在与 Internet URI(统一资源标识符)相关联的程序(脚本)动态生成的内容上。4假设我们在客户端计算机上的浏览器中输入 URI,然后浏览器向相应的 Web 服务器发送请求。如果内容是动态创建的,那么显然第一个问题是:创建它的脚本是在服务器上运行还是在客户端计算机上运行?这些选项分别称为服务器端和客户端Web 脚本。
From a programming languages point of view, simple playback of recorded audio or video is not particularly interesting. We therefore focus our attention here on content that is generated on the fly by a program—a script—associated with an Internet URI (uniform resource identifier).4 Suppose we type a URI into a browser on a client machine, and the browser sends a request to the appropriate web server. If the content is dynamically created, an obvious first question is: does the script that creates it run on the server or the client machine? These options are known as server-side and client-side web scripting, respectively.
服务器端脚本通常用于服务提供商希望完全控制页面内容,但无法(或不想)提前创建内容的情况。示例包括搜索引擎、互联网零售商、拍卖网站以及任何为客户提供个人帐户在线访问权限的组织返回的页面。客户端脚本通常用于不需要访问专有信息的任务,如果在客户端的机器上执行,效率会更高。示例包括交互式动画、填写表格的错误检查以及各种其他独立计算。
Server-side scripts are typically used when the service provider wants to retain complete control over the content of the page, but can't (or doesn't want to) create the content in advance. Examples include the pages returned by search engines, Internet retailers, auction sites, and any organization that provides its clients with on-line access to personal accounts. Client-side scripts are typically used for tasks that don't need access to proprietary information, and are more efficient if executed on the client's machine. Examples include interactive animation, error-checking of fill-in forms, and a wide variety of other self-contained calculations.
服务器端 Web 脚本的原始机制是通用网关接口 (CGI)。CGI 脚本是驻留在 Web 服务器程序已知的特殊目录中的可执行程序。当客户端请求与此类程序相对应的 URI 时,服务器将执行该程序并将其输出发送回客户端。当然,此输出必须是浏览器可以理解的内容 — 通常是 HTML。
The original mechanism for server-side web scripting was the Common Gateway Interface (CGI). A CGI script is an executable program residing in a special directory known to the web server program. When a client requests the URI corresponding to such a program, the server executes the program and sends its output back to the client. Naturally, this output needs to be something that the browser will understand—typically HTML.
尽管 CGI 脚本被广泛使用,但它有几个缺点:
Though widely used, CGI scripts have several disadvantages:
■ Web 服务器必须将每个脚本作为单独的程序启动,这可能会带来很大的开销(尽管编译为本机代码的 CGI 脚本一旦运行就会非常快)。
■ The web server must launch each script as a separate program, with potentially significant overhead (though a CGI script compiled to native code can be very fast once running).
■ 因为服务器几乎无法控制脚本的行为,所以脚本通常必须由受信任的系统管理员安装在受信任的目录中;它们不能像普通页面一样驻留在任意位置。
■ Because the server has little control over the behavior of a script, scripts must generally be installed in a trusted directory by trusted system administrators; they cannot reside in arbitrary locations as ordinary pages do.
■ 脚本的名称出现在URI中,通常以受信任目录的名称作为前缀,因此静态和动态页面对于最终用户来说看起来是不同的。
■ The name of the script appears in the URI, typically prefixed with the name of the trusted directory, so static and dynamic pages look different to end users.
■ 每个脚本不仅必须生成动态内容,还必须生成格式化和显示动态内容所需的 HTML 标记。这些额外的“样板”使得脚本更难编写。
■ Each script must generate not only dynamic content but also the HTML tags that are needed to format and display it. This extra “boilerplate” makes scripts more difficult to write.
为了解决这些缺点,大多数 Web 服务器都提供了“模块加载”机制,允许将一种或多种脚本语言的解释器合并到服务器本身中。然后可以将受支持语言的脚本嵌入到“普通”网页中。Web 服务器直接解释此类脚本,而无需启动外部程序。然后,它会用脚本生成的输出替换它们,然后再将页面发送到客户端。客户端甚至无法知道脚本的存在。
To address these disadvantages, most web servers provide a “module-loading” mechanism that allows interpreters for one or more scripting languages to be incorporated into the server itself. Scripts in the supported language(s) can then be embedded in “ordinary” web pages. The web server interprets such scripts directly, without launching an external program. It then replaces the scripts with the output they produce, before sending the page to the client. Clients have no way to even know that the scripts exist.
可嵌入的服务器端脚本语言包括 PHP、PowerShell(在 Microsoft Active Server Pages 中)、Ruby、Cold Fusion(来自 Macromedia Corp.)和 Java(通过 Java Server Pages 中的“Servlet”)。其中最常见的是 PHP。尽管 PHP 源自 Perl,但它已针对其目标域进行了广泛的定制,内置了对电子邮件和 MIME 编码、所有标准 Internet 通信协议、身份验证和安全、HTML 和 URI 操作以及与数十个数据库系统的交互等的支持。
Embeddable server-side scripting languages include PHP, PowerShell (in Microsoft Active Server Pages), Ruby, Cold Fusion (from Macromedia Corp.), and Java (via “Servlets” in Java Server Pages). The most common of these is PHP. Though descended from Perl, PHP has been extensively customized for its target domain, with built-in support for (among other things) e-mail and MIME encoding, all the standard Internet communication protocols, authentication and security, HTML and URI manipulation, and interaction with dozens of database systems.
虽然嵌入式服务器端脚本通常比 CGI 脚本更快,至少在启动成本占主导地位时,但 Internet 上的通信对于真正交互式的页面来说仍然太慢。如果我们想让页面的行为或外观随着用户移动鼠标、点击、键入或隐藏或显示窗口而改变,我们确实需要在客户端计算机上执行某种脚本。
While embedded server-side scripts are generally faster than CGI scripts, at least when start-up cost predominates, communication across the Internet is still too slow for truly interactive pages. If we want the behavior or appearance of the page to change as the user moves the mouse, clicks, types, or hides or exposes windows, we really need to execute some sort of script on the client's machine.
由于 CGI 脚本和可嵌入的服务器端脚本(在较小程度上)在 Web 设计人员的站点上运行,因此它们可以用多种不同的语言编写。客户端看到的只是标准 HTML。相比之下,客户端脚本需要客户端机器上的解释器。由于 JavaScript 历来“在正确的时间出现在正确的地点”,因此几乎世界上所有的 Web 浏览器都至少在一定程度上一致地支持 JavaScript。鉴于仍在运行的旧版浏览器数量,以及说服用户升级或安装新插件的难度,旨在供有限域(例如,单个公司的桌面)之外使用的页面几乎总是使用 JavaScript 来实现交互功能。
Because they run on the web designer's site, CGI scripts and, to a lesser extent, embeddable server-side scripts can be written in many different languages. All the client ever sees is standard HTML. Client-side scripts, by contrast, require an interpreter on the client's machine. By virtue of having been “in the right place at the right time” historically, JavaScript is supported with at least some degree of consistency by almost all of the world's web browsers. Given the number of legacy browsers still running, and the difficulty of convincing users to upgrade or to install new plug-ins, pages intended for use outside a limited domain (e.g., the desktops of a single company) almost always use JavaScript for interactive features.
作为要求客户端脚本与网页的 DOM 交互的替代方案,许多浏览器都支持嵌入机制,该机制允许浏览器插件负责页面的某些矩形区域,然后它可以在该区域显示所需的任何内容。换句话说,插件与其说是编写浏览器脚本,不如说是完全绕过浏览器。从历史上看,插件广泛用于 HTML 不太支持的内容(尤其是动画和视频)。
As an alternative to requiring client-side scripts to interact with the DOM of a web page, many browsers support an embedding mechanism that allows a browser plug-in to assume responsibility for some rectangular region of the page, in which it can then display whatever it wants. In other words, plug-ins are less a matter of scripting the browser than of bypassing it entirely. Historically, plug-ins were widely used for content—animations and video in particular—that were poorly supported by HTML.
从type属性的存在可以推断,embed标签可以请求各种插件执行,而不仅仅是 Java 虚拟机。截至 2015 年,使用最广泛的插件是 Adobe 的 Flash Player。虽然可以编写脚本,与通用编程语言解释器相比,Flash Player 更准确地说是一个多媒体显示引擎。
As one might infer from the existence of the type attribute, embed tags can request execution by a wide variety of plug-ins—not just a Java Virtual Machine. As of 2015, the most widely used plug-in is Adobe's Flash Player. Though scriptable, Flash Player is more accurately described as a multimedia display engine than a general purpose programming language interpreter.
随着时间的推移,插件已被证明是浏览器安全漏洞的主要来源。几乎任何重要的插件都需要访问操作系统服务 — 网络 IO、本地文件空间、图形加速等等。提供刚好够用的服务 — 但又不至于造成任何危害 — 已被证明是极其困难的。为了解决这个问题,HTML5 标准中内置了广泛的多媒体支持,允许浏览器本身承担曾经通过插件完成的大部分工作。安全性仍然是一个问题,但必须信任的软件模块数量 — 以及攻击者可能试图进入的点数 — 已显著减少。许多浏览器现在默认禁用 Java。有些浏览器还禁用 Flash。
Over time, plug-ins have proven to be a major source of browser security bugs. Almost any nontrivial plug-in requires access to operating system services—network IO, local file space, graphics acceleration, and so on. Providing just enough service to make the plug-in useful—but not enough to allow it to do any harm—has proven extremely difficult. To address this problem, extensive multimedia support has been built into the HTML5 standard, allowing the browser itself to assume responsibility for much of what was once accomplished with plugins. Security is still a problem, but the number of software modules that must be trusted—and the number of points at which an attacker might try to gain entrance—is significantly reduced. Many browsers now disable Java by default. Some disable Flash as well.
毫无疑问,大多数读者都有机会编写或至少阅读用于编写网页的 HTML(超文本标记语言)。HTML 在大多数情况下都具有嵌套结构,其中文档片段(元素)由指示其用途或外观的标记分隔。例如,我们在14.2.2 节中看到,顶级标题用<h1>和</h1>分隔。不幸的是,由于 Web 的发展方式混乱且不规范,HTML 在设计上存在许多不一致之处,并且不同供应商实现的版本之间存在不兼容性。
Most readers will undoubtedly have had the opportunity to write, or at least to read, the HTML (hypertext markup language) used to compose web pages. HTML has, for the most part, a nested structure in which fragments of documents (elements) are delimited by tags that indicate their purpose or appearance. We saw in Section 14.2.2, for example, that top-level headings are delimited with <h1> and </h1>. Unfortunately, as a result of the chaotic and informal way in which the Web evolved, HTML ended up with many inconsistencies in its design, and incompatibilities among the versions implemented by different vendors.
XML(可扩展标记语言)是一种较新且通用的语言,可用于捕获结构化数据。与 HTML 相比,它的语法和语义更规则、更一致,并且在各个平台上的实现也更一致。它是可扩展的,这意味着用户可以定义自己的标签。它还明确区分了文档的内容(它捕获的数据)和数据的呈现。实际上,呈现被推迟到称为 XSL(可扩展样式表语言)的配套标准。XSLT 是 XSL 的一部分,专用于转换XML:选择、重新组织和修改标签及其分隔的元素 — 实际上,就是编写脚本来处理以 XML 表示的数据。
XML (extensible markup language) is a more recent and general language in which to capture structured data. Compared to HTML, its syntax and semantics are more regular and consistent, and more consistently implemented across platforms. It is extensible, meaning that users can define their own tags. It also makes a clear distinction between the content of a document (the data it captures) and the presentation of that data. Presentation, in fact, is deferred to a companion standard known as XSL (extensible stylesheet language). XSLT is a portion of XSL devoted to transforming XML: selecting, reorganizing, and modifying tags and the elements they delimit—in effect, scripting the processing of data represented in XML.
更深入地
IN MORE DEPTH
XML 可用于为非常广泛的应用领域创建专用标记语言。XHTML 是符合 XML 标准的 HTML 的几乎(但不完全)向后兼容的变体。越来越多的 Web 工具被设计用于生成 XHTML。
XML can be used to create specialized markup languages for a very wide range of application domains. XHTML is an almost (but not quite) backward compatible variant of HTML that conforms to the XML standard. Web tools are increasingly being designed to generate XHTML.
在配套网站上,我们讨论了与 XML 相关的各种主题,并特别强调了 XSLT。我们详细阐述了内容和表示之间的区别,介绍了样式表语言的一般概念,并以XHTML 为例,描述了用于定义特定领域 XML 应用程序的文档类型定义(DTD) 和架构。
On the companion site, we consider a variety of topics related to XML, with a particular emphasis on XSLT. We elaborate on the distinction between content and presentation, introduce the general notion of stylesheet languages, and describe the document type definitions (DTDs) and schemas used to define domain-specific applications of XML, using XHTML as an example.
由于标签需要嵌套,因此 XML 文档具有自然的树状结构。XSLT 旨在通过递归遍历来处理这些树。虽然它可以用于几乎任何以 XML 作为输入的任务,但其最常见的用途可能是将 XML 转换为格式化的输出(通常是 XHTML)以在浏览器中显示。作为一个扩展示例,我们考虑基于 XML 的书目数据库的格式化。
Because tags are required to nest, an XML document has a natural tree-based structure. XSLT is designed to process these trees via recursive traversal. Though it can be used for almost any task that takes XML as input, perhaps its most common use is to transform XML into formatted output—often XHTML to be presented in a browser. As an extended example, we consider the formatting of an XML-based bibliographic database.
在14.1.1 节中,我们列出了脚本语言的几个共同特征:
In Section 14.1.1, we listed several common characteristics of scripting languages:
1. Both batch and interactive use
2. 表达经济
2. Economy of expression
3. 缺乏声明;作用域规则简单
3. Lack of declarations; simple scoping rules
4. 灵活的动态类型
4. Flexible dynamic typing
5. 轻松访问其他程序
5. Easy access to other programs
6. 复杂的模式匹配和字符串操作
6. Sophisticated pattern matching and string manipulation
7. 高级数据类型
7. High-level data types
以下小节将更详细地讨论其中的几个。具体来说,第 14.4.1 节讨论了脚本语言中的命名和作用域;第 14.4.2 节讨论了字符串和模式操作;第 14.4.3 节讨论了数据类型。我们列表中的项目 (1)、(2) 和 (5) 虽然很重要,但并不是特别困难或微妙,因此这里不再赘述。
Several of these are discussed in more detail in the subsections below. Specifically, Section 14.4.1 considers naming and scoping in scripting languages; Section 14.4.2 discusses string and pattern manipulation; and Section 14.4.3 considers data types. Items (1), (2), and (5) in our list, while important, are not particularly difficult or subtle, and will not be considered further here.
大多数脚本语言(Scheme 是明显的例外)不需要声明变量。少数语言(尤其是 Perl 和 JavaScript)允许可选声明,主要作为一种编译器检查文档。Perl 可以在需要声明的模式下运行(使用严格的“vars”)。无论有没有声明,大多数脚本语言都使用动态类型。值通常是自描述的,因此解释器可以在运行时执行类型检查,或在适当的时候强制值。
Most scripting languages (Scheme is the obvious exception) do not require variables to be declared. A few languages, notably Perl and JavaScript, permit optional declarations, primarily as a sort of compiler-checked documentation. Perl can be run in a mode (use strict ' vars') that requires declarations. With or without declarations, most scripting languages use dynamic typing. Values are generally self-descriptive, so the interpreter can perform type checking at run time, or coerce values when appropriate.
嵌套和作用域约定差异很大。Scheme、Python、JavaScript 和 R 提供了嵌套子程序和静态(词法)作用域的经典组合。Tcl 允许子程序嵌套,但使用动态作用域。命名子程序(方法)在 PHP 或 Ruby 中不嵌套,在 Perl 中也只是某种程度上的嵌套(下面还会详细介绍),但 Perl 和 Ruby 与 Scheme、Python、JavaScript 和 R 一起提供了一流的匿名本地子程序。嵌套块在 Perl 中是静态作用域。在 Ruby 中,它们是它们出现的命名作用域的一部分。Scheme、Perl、Python、Ruby、JavaScript 和 R 都为闭包中捕获的变量提供了无限范围。PHP、R 和主要的粘合语言(Perl、Tcl、Python、Ruby)都具有复杂的命名空间机制,用于信息隐藏和从单独的模块选择性导入名称。
Nesting and scoping conventions vary quite a bit. Scheme, Python, JavaScript, and R provide the classic combination of nested subroutines and static (lexical) scope. Tcl allows subroutines to nest, but uses dynamic scoping. Named subroutines (methods) do not nest in PHP or Ruby, and they are only sort of nest in Perl (more on this below as well), but Perl and Ruby join Scheme, Python, JavaScript, and R in providing first-class anonymous local subroutines. Nested blocks are statically scoped in Perl. In Ruby, they are part of the named scope in which they appear. Scheme, Perl, Python, Ruby, JavaScript, and R all provide unlimited extent for variables captured in closures. PHP, R, and the major glue languages (Perl, Tcl, Python, Ruby) all have sophisticated namespace mechanisms for information hiding and the selective import of names from separate modules.
在具有静态作用域的语言中,缺少声明引发了一个有趣的问题:当我们访问变量x时,我们如何知道它是本地的、全局的,还是(如果作用域可以嵌套)介于两者之间的某个变量?现有的语言采用了几种不同的方法。在 Perl 中,除非明确声明,否则所有变量都是全局的。在 PHP 中,除非明确导入,否则它们是本地的(并且所有导入都是全局的,因为作用域不嵌套)。Ruby 也只有两个实际的作用域级别,但正如我们在14.2.4 节中看到的,它使用名称上的前缀字符来区分它们:foo是本地变量;$foo是全局变量;@foo是当前对象(其方法当前正在执行的对象)的实例变量;@@foo是当前对象类的实例变量(由所有兄弟实例共享)。(注意:正如我们将在14.4.3 节中看到的,Perl 使用类似的前缀字符来指示类型。这些非常不同的用法可能会让在两种语言之间切换的程序员感到困惑。)
In languages with static scoping, the lack of declarations raises an interesting question: when we access a variable x, how do we know if it is local, global, or (if scopes can nest) something in-between? Existing languages take several different approaches. In Perl, all variables are global unless explicitly declared. In PHP, they are local unless explicitly imported (and all imports are global, since scopes do not nest). Ruby, too, has only two real levels of scoping, but as we saw in Section 14.2.4, it distinguishes between them using prefix characters on names: foo is a local variable; $foo is a global variable; @foo is an instance variable of the current object (the one whose method is currently executing); @@foo is an instance variable of the current object's class (shared by all sibling instances). (Note: as we shall see in Section 14.4.3, Perl uses similar prefix characters to indicate type. These very different uses are a potential source of confusion for programmers who switch between the two languages.)
Perl 多年来一直在发展。起初,只有全局变量。为了模块化,很快就添加了局部变量,因此带有名为i的变量的子例程不必担心修改代码中其他地方需要的全局i。不幸的是,局部变量最初是根据动态作用域定义的,而向后兼容的需要要求在 Perl 5 中添加静态作用域时保留此行为。因此,该语言提供了这两种机制。
Perl has evolved over the years. At first, there were only global variables. Locals were soon added for the sake of modularity, so a subroutine with a variable named i wouldn't have to worry about modifying a global i that was needed elsewhere in the code. Unfortunately, locals were originally defined in terms of dynamic scoping, and the need for backward compatibility required that this behavior be retained when static scoping was added in Perl 5. Consequently, the language provides both mechanisms.
当我们在2.1.1 节中第一次考虑正则表达式时,我们注意到许多脚本语言和相关工具都使用了该符号的扩展版本。一些扩展只是为了方便。其他扩展则增加了符号的表达能力,使我们能够生成(匹配)非正则字符串集。还有一些扩展用于将符号与其他语言特性联系起来。
When we first considered regular expressions, in Section 2.1.1, we noted that many scripting languages and related tools employ extended versions of the notation. Some extensions are simply a matter of convenience. Others increase the expressive power of the notation, allowing us to generate (match) nonregular sets of strings. Still other extensions serve to tie the notation to other language features.
我们已经在sed(图 14.1)、awk(图 14.2和14.3)、Perl(图 14.4和14.5)、Python(图 14.6)和 Ruby(图 14.7)中看到了扩展正则表达式的示例。许多读者也熟悉grep,它是独立的 Unix 模式匹配工具(参见边栏 14.8)。
We have already seen examples of extended regular expressions in sed (Figure 14.1), awk (Figures 14.2 and 14.3), Perl (Figures 14.4 and 14.5), Python (Figure 14.6), and Ruby (Figure 14.7). Many readers will also be familiar with grep, the stand-alone Unix pattern-matching tool (see Sidebar 14.8).
扩展正则表达式 (简称“RE”) 有很多不同的实现,而且语法略有不同,但大多数都分为两大类。第一类包括awk、egrep (在grep的几个不同版本中使用最广泛的)和C 的regex库。它们实现了 POSIX 标准 [ Int03b ] 中定义的正则表达式。第二类语言以 Perl 为榜样,Perl 提供了一大套扩展,有时被称为“高级正则表达式”。类似 Perl 的高级正则表达式出现在 PHP、Python、Ruby、JavaScript、Emacs Lisp、Java 和 C# 中。在 C++ 和其他语言的第三方软件包中也可以找到它们。一些工具,包括sed、经典grep和较旧的 Unix 编辑器,提供所谓的“基本”正则表达式,功能不如egrep 的正则表达式。
While there are many different implementations of extended regular expressions (“REs” for short), with slightly different syntax, most fall into two main groups. The first group includes awk, egrep (the most widely used of several different versions of grep), and the regex library for C. These implement REs as defined in the POSIX standard [Int03b]. Languages in the second group follow the lead of Perl, which provides a large set of extensions, sometimes referred to as “advanced REs.” Perl-like advanced REs appear in PHP, Python, Ruby, JavaScript, Emacs Lisp, Java, and C#. They can also be found in third-party packages for C++ and other languages. A few tools, including sed, classic grep, and older Unix editors, provide so-called “basic” REs, less capable than those of egrep.
在某些语言和工具中(尤其是sed、awk、Perl、PHP、Ruby 和 JavaScript),正则表达式与语言的其余部分紧密集成,具有特殊语法和内置运算符。在这些语言中,正则表达式通常用斜杠字符分隔,但在某些情况下也可以接受其他分隔符(事实上,Perl 为一些备选分隔符提供了略有不同的语义)。在大多数其他语言中,正则表达式表示为普通字符串,并通过将它们传递给库例程来操作。在接下来的几页中,我们将更详细地考虑 POSIX 和高级正则表达式。在介绍 Perl 之后,我们将使用斜杠作为分隔符。我们的介绍必然是不完整的。Perl 一书中有关正则表达式的章节 [ CfWO12,第 5 章] 超过 100 页。相应的 Unix手册页总共大约 40 页。
In certain languages and tools—notably sed, awk, Perl, PHP, Ruby, and JavaScript—regular expressions are tightly integrated into the rest of the language, with special syntax and built-in operators. In these languages an RE is typically delimited with slash characters, though other delimiters maybe accepted in some cases (and Perl in fact provides slightly different semantics for a few alternative delimiters). In most other languages, REs are expressed as ordinary character strings, and are manipulated by passing them to library routines. Over the next few pages we will consider POSIX and advanced REs in more detail. Following Perl, we will use slashes as delimiters. Our coverage will of necessity be incomplete. The chapter on REs in the Perl book [CfWO12, Chap. 5] is over 100 pages long. The corresponding Unix man page totals some 40 pages.
对于多行字符串中的匹配,尾随的s允许点 (.) 匹配嵌入的换行符(通常无法匹配)。尾随的m允许$和^分别匹配此类换行符之前和之后的换行符。尾随的x使 Perl 忽略模式中的注释和嵌入的空格,这样可以将特别复杂的表达式拆分为多行、记录和缩进。
For matching in multiline strings, a trailing s allows a dot (.) to match an embedded newline (which it normally cannot). A trailing m allows $ and ^ to match immediately before and after such a newline, respectively. A trailing x causes Perl to ignore both comments and embedded white space in the pattern so that particularly complicated expressions can be broken across multiple lines, documented, and indented.
按照 C 及其相关语言(示例 8.29 )的传统,Perl 允许使用反斜杠转义序列在 RE 中指定非打印字符。一些最常用的示例出现在图 14.18的上部。除了标准的 ^ 和 $ 之外,Perl 还提供了几个零宽度断言。示例显示在图的中间。\A和\Z转义与 ^ 和 $ 不同,因为它们分别只在字符串的开头和结尾继续匹配,即使在使用修饰符m 的多行搜索中也是如此。最后,Perl 提供了几个内置字符类,其中一些显示在图的底部。这些可以在用户定义(即括号分隔)类的内部和外部使用。请注意,\ b在这些类内部和外部具有不同的含义。
In the tradition of C and its relatives (Example 8.29), Perl allows nonprinting characters to be specified in REs using backslash escape sequences. Some of the most frequently used examples appear in the top portion of Figure 14.18. Perl also provides several zero-width assertions, in addition to the standard ^ and $. Examples are shown in the middle of the figure. The \A and \Z escapes differ from ^ and $ in that they continue to match only at the beginning and end of the string, respectively, even in multiline searches that use the modifier m. Finally, Perl provides several built-in character classes, some of which are shown at the bottom of the figure. These can be used both inside and outside user-defined (i.e., bracket-delimited) classes. Note that \b has different meanings inside and outside such classes.
正如这些示例所示,Perl(以及 Tcl)使用变量的值模型。Scheme、Python 和 Ruby 使用引用模型。PHP 和 JavaScript 与 Java 一样,对原始类型的变量使用值模型,对对象类型的变量使用引用模型。这种区别在 PHP 和 JavaScript 中不如在 Java 中那么重要,因为同一个变量可以在一个时间点保存原始值,而在另一个时间点保存对象引用。
As these examples suggest, Perl (and likewise Tcl) uses a value model of variables. Scheme, Python, and Ruby use a reference model. PHP and JavaScript, like Java, use a value model for variables of primitive type and a reference model for variables of object type. The distinction is less important in PHP and JavaScript than it is in Java, because the same variable can hold a primitive value at one point in time and an object reference at another.
正如我们在14.4.2 节中看到的,脚本语言通常提供一组非常丰富的字符串和模式操作机制。语法和插值约定各不相同,但底层功能非常一致,并且深受 Perl 的影响。对数值类型的底层支持在不同语言中表现出更多的差异,但编程模型同样非常一致:大致上,鼓励用户将数值视为“简单的数字”,而不必担心定点和浮点之间的区别,也不必担心可用精度的限制。
As we have seen in Section 14.4.2, scripting languages generally provide a very rich set of mechanisms for string and pattern manipulation. Syntax and interpolation conventions vary, but the underlying functionality is remarkably consistent, and heavily influenced by Perl. The underlying support for numeric types shows a bit more variation across languages, but the programming model is again remarkably consistent: users are, to first approximation, encouraged to think of numeric values as “simply numbers,” and not to worry about the distinction between fixed and floating point, or about the limits of available precision.
在内部,JavaScript 中的数字始终是双精度浮点数;在 Lua 中,它们默认也是双精度数。在 Tcl 中,它们是字符串,在需要算术时转换为整数或浮点数(并再次转换回来)。PHP 使用整数(保证至少 32 位宽),加上双精度浮点数。Perl 和 Ruby 为这些数字添加了任意精度(多字)整数,有时称为bignum s。Python 也有 bignums,并且支持复数。Scheme 具有上述所有功能,以及精确有理数,以 (分子,分母) 对的形式维护。在所有情况下,解释器在对具有不同表示形式的值进行算术运算时,或者在发生溢出时,都会根据需要进行“向上转换”。
Internally, numbers in JavaScript are always double-precision floating point; they are doubles by default in Lua as well. In Tcl they are strings, converted to integers or floating-point numbers (and back again) when arithmetic is needed. PHP uses integers (guaranteed to be at least 32 bits wide), plus double-precision floating point. To these Perl and Ruby add arbitrary precision (multiword) integers, sometimes known as bignums. Python has bignums too, plus support for complex numbers. Scheme has all of the above, plus precise rationals, maintained as (numerator, denominator) pairs. In all cases the interpreter “up-converts” as necessary when doing arithmetic on values with different representations, or when overflow would otherwise occur.
Perl 非常谨慎地隐藏了不同数字表示法之间的区别。大多数其他语言允许用户决定使用哪种表示法,尽管这很少是必要的。Ruby 可能对不同表示法的存在最为明确:类Fixnum、Bignum和Float(双精度浮点)具有重叠但不完全相同的内置方法集。特别是,整数具有迭代器方法,而浮点数没有,浮点数具有舍入和错误检查方法,而整数没有。Fixnum和Bignum都是Integer的后代。
Perl is scrupulous about hiding the distinctions among different numeric representations. Most other languages allow the user to determine which is being used, though this is seldom necessary. Ruby is perhaps the most explicit about the existence of different representations: classes Fixnum, Bignum, and Float (double-precision floating point) have overlapping but not identical sets of built-in methods. In particular, integers have iterator methods, which floating-point numbers do not, and floating-point numbers have rounding and error checking methods, which integers do not. Fixnum and Bignum are both descendants of Integer.
选择 C、Fortran 和 Ada 等编译语言的类型构造函数主要是为了高效实现。特别是数组和记录,它们具有直接的时间和空间效率实现,我们在第8 章中学习过。然而,效率在脚本语言中并不那么重要。设计人员可以自由选择类型构造函数,这些构造函数更注重易于理解而不是纯粹的运行时性能。特别是,大多数脚本语言都非常重视映射,有时也称为字典、哈希或关联数组。从这些名称中的第三个可能猜出,映射通常是用哈希表实现的。哈希的访问时间仍然为 0(1),但其常数明显高于编译数组或记录的典型值。
The type constructors of compiled languages like C, Fortran, and Ada were chosen largely for the sake of efficient implementation. Arrays and records, in particular, have straightforward time- and space-efficient implementations, which we studied in Chapter 8. Efficiency, however, is less important in scripting languages. Designers have felt free to choose type constructors oriented more toward ease of understanding than pure run-time performance. In particular, most scripting languages place a heavy emphasis on mappings, sometimes called dictionaries, hashes, or associative arrays. As might be guessed from the third of these names, a mapping is typically implemented with a hash table. Access time for a hash remains 0(1), but with a significantly higher constant than is typical for a compiled array or record.
Perl 是使用最广泛的脚本语言中历史最悠久的一种,它从awk继承了其主要的复合类型(数组和哈希) 。它还在变量名上使用前缀字符来指示类型:$foo是标量(数字、布尔值、字符串或指针 [Perl 称之为“引用”]);@foo是数组;%foo是哈希;&foo是子例程;而普通的foo是文件句柄或 I/O 格式,具体取决于上下文。
Perl, the oldest of the widely used scripting languages, inherits its principal composite types—the array and the hash—from awk. It also uses prefix characters on variable names as an indication of type: $foo is a scalar (a number, Boolean, string, or pointer [which Perl calls a “reference”]); @foo is an array; %foo is a hash; &foo is a subroutine; and plain foo is a filehandle or an I/O format, depending on context.
在7.2.2 节中,我们定义了类型兼容性的概念,它决定了在静态类型语言中,哪些类型可以在哪些上下文中使用。在这个定义中,术语“上下文”是指有关如何使用值的信息。例如,在 C 语言中,人们可能会说在声明中
In Section 7.2.2 we defined the notion of type compatibility, which determines, in a statically typed language, which types can be used in which contexts. In this definition the term “context” refers to information about how a value will be used. In C, for example, one might say that in the declaration
右侧的3出现在需要浮点数的上下文中。C 编译器将3强制转换为double而不是int。
the 3 on the right-hand side occurs in a context that expects a floating-point number. The C compiler coerces the 3 to make it a double instead of an int.
在7.2.3 节中,我们继续定义了类型推断的概念,它允许编译器根据表达式的组成部分的类型以及在某些情况下出现的上下文来确定表达式的类型。我们在 ML 及其后代中看到了一个极端的例子,它们使用一种复杂的推断形式来确定大多数对象的类型,而无需声明。
In Section 7.2.3 we went on to define the notion of type inference, which allows a compiler to determine the type of an expression based on the types of its constituent parts and, in some cases, the context in which it appears. We saw an extreme example in ML and its descendants, which use a sophisticated form of inference to determine types for most objects without the need for declarations.
在兼容性和推理这两种情况下,上下文信息仅在编译时使用。Perl 扩展了上下文的概念,以驱动运行时做出的决策。更具体地说,Perl 中的每个运算符在编译时确定其每个参数是否应将该参数解释为标量或列表。相反,每个参数(本身可能是嵌套运算符)都能够在运行时判断它占用哪种上下文,从而可以表现出不同的行为。
In both of these cases—compatibility and inference—contextual information is used at compile time only. Perl extends the notion of context to drive decisions made at run time. More specifically, each operator in Perl determines, at compile time, and for each of its arguments, whether that argument should be interpreted as a scalar or a list. Conversely each argument (which may itself be a nested operator) is able to tell, at run time, which kind of context it occupies, and can consequently exhibit different behavior.
虽然 Perl 5 不是面向对象的语言,但它具有允许以面向对象风格进行编程的功能。7 PHP和 JavaScript 具有更简洁、更传统的面向对象功能,但两者都允许程序员使用更传统的命令式风格。Python 和 Ruby 是明确且统一的面向对象语言。
Though not an object-oriented language, Perl 5 has features that allow one to program in an object-oriented style.7 PHP and JavaScript have cleaner, more conventional-looking object-oriented features, but both allow the programmer to use a more traditional imperative style as well. Python and Ruby are explicitly and uniformly object-oriented.
Perl 使用变量值模型;对象始终通过指针访问。在 PHP 和 JavaScript 中,变量可以保存原始类型的值或对复合类型对象的引用。然而,与 Perl 不同的是,这些语言没有提供方法来表示引用本身,而只提供它所引用的对象。Python 和 Ruby 使用统一的引用模型。
Perl uses a value model for variables; objects are always accessed via pointers. In PHP and JavaScript, a variable can hold either a value of a primitive type or a reference to an object of composite type. In contrast to Perl, however, these languages provide no way to speak of the reference itself, only the object to which it refers. Python and Ruby use a uniform reference model.
在 Python 和 Ruby 中,类本身就是对象,就像在 Smalltalk 中一样。在 PHP 中,它们只是类型,就像在 C++、Java 或 C# 中一样。在 Perl 中,类只是查看包(命名空间)的另一种方式。JavaScript 有对象但没有类,这一点很特别;它的继承基于一个称为原型的概念,该概念最初由 Self 编程语言引入。
Classes are themselves objects in Python and Ruby, much as they are in Smalltalk. They are merely types in PHP, much as they are in C++, Java, or C#. Classes in Perl are simply an alternative way of looking at packages (namespaces). JavaScript, remarkably, has objects but no classes; its inheritance is based on a concept known as prototypes, initially introduced by the Self programming language.
Perl 5 中的对象支持归结为两个主要方面:(1) 将引用与包关联的“祝福”机制,以及 (2) 方法调用的特殊语法,可自动将对象引用或包名称作为初始参数传递给函数。虽然原则上任何引用都可以得到祝福,但通常的惯例是使用哈希,以便可以命名字段,如示例 14.63所示。
Object support in Perl 5 boils down to two main things: (1) a “blessing” mechanism that associates a reference with a package, and (2) special syntax for method calls that automatically passes an object reference or package name as the initial argument to a function. While any reference can in principle be blessed, the usual convention is to use a hash, so that fields can be named as shown in Example 14.63.
虽然 Perl 的机制足以创建面向对象程序,但动态查找使它们比同等的命令式程序慢,而且可以说语法不够优雅。对象对 PHP 和 JavaScript 来说更为根本。
While Perl's mechanisms suffice to create object-oriented programs, dynamic lookup makes them slower than equivalent imperative programs, and it seems fair to characterize the syntax as less than elegant. Objects are more fundamental to PHP and JavaScript.
PHP 4 提供了多种面向对象功能,这些功能在 PHP 5 中进行了重大修改。新版本的语言提供了 (类类型) 变量、接口和混合继承、抽象方法和类、最终方法和类、静态和常量成员以及访问控制说明符 ( public、protected和private ) 的参考模型,让人联想到 Java、C# 和 C++。与本小节讨论的所有其他语言不同,PHP 中的类声明必须包含所有成员 (字段和方法) 的声明,并且给定类中的成员集不能随后更改 (尽管当然可以声明具有其他成员的派生类)。
PHP 4 provided a variety of object-oriented features, which were heavily revised in PHP 5. The newer version of the language provides a reference model of (class-typed) variables, interfaces and mix-in inheritance, abstract methods and classes, final methods and classes, static and constant members, and access control specifiers (public, protected, and private) reminiscent of those of Java, C#, and C++. In contrast to all other languages discussed in this subsection, class declarations in PHP must include declarations of all members (fields and methods), and the set of members in a given class cannot subsequently change (though one can of course declare derived classes with additional members).
JavaScript 采用了一种不寻常的方法,它通过继承和动态方法分派来提供对象,而不提供类。这种语言被称为基于对象的,而不是面向对象的 。在 JavaScript 中,函数是一等实体,实际上是对象。方法只是一个由对象的属性(成员)引用的函数。当我们调用om时,关键字this将在m引用的函数执行期间引用o。同样,当我们调用new f时,this将在f执行期间引用一个新创建的(最初为空的)对象。因此,JavaScript 中的构造函数是一个函数,其目的是将值分配给新创建对象的属性(字段和方法)。
JavaScript takes the unusual approach of providing objects—with inheritance and dynamic method dispatch—without providing classes. Such a language is said to be object-based, as opposed to object-oriented. Functions are first-class entities in JavaScript—objects, in fact. A method is simply a function that is referred to by a property (member) of an object. When we call o.m, the keyword this will refer to o during the execution of the function referred to by m. Likewise when we call new f, this will refer to a newly created (initially empty) object during the execution of f. A constructor in JavaScript is thus a function whose purpose is to assign values into properties (fields and methods) of a newly created object.
与每个构造函数f关联的是一个对象f.prototype。如果对象o由f构造,那么每当我们尝试使用 o 本身不提供的属性时,JavaScript 都会在 f.prototype 中查找。实际上,o会从f.prototype 继承它未覆盖的任何内容。原型属性通常用于保存方法。它们也可以用于常量或其他语言所称的“类变量”。
Associated with every constructor f is an object f.prototype. If object o was constructed by f, then JavaScript will look in f.prototype whenever we attempt to use a property of o that o itself does not provide. In effect, o inherits from f.prototype anything that it does not override. Prototype properties are commonly used to hold methods. They can also be used for constants or for what other languages would call “class variables.”
ECMAScript 6 预计将于 2015 年正式发布,它为该语言添加了正式的类概念以及许多其他功能。类以向后兼容的方式定义 - 本质上是带有原型的构造函数的语法糖。
ECMAScript 6, expected to become official in 2015, adds a formal notion of classes to the language, along with a host of other features. Classes are defined in a backward compatible way—essentially as syntactic sugar for constructors with prototypes.
正如我们指出的,Python 和 Ruby 都是明确的面向对象的。两者都采用统一的变量引用模型。与 Smalltalk 一样,两者都包含一个对象层次结构,其中类本身由对象表示。Python 中的根类称为object;在 Ruby 中,它是Object。
As we have noted, both Python and Ruby are explicitly object-oriented. Both employ a uniform reference model for variables. Like Smalltalk, both incorporate an object hierarchy in which classes themselves are represented by objects. The root class in Python is called object; in Ruby it is Object.
就类的内容(成员)而言,Python 和 Ruby 都比 PHP 或更传统的面向对象语言更灵活。只需将新字段分配给它们即可将其添加到 Python 对象中:my_object.new_field = value。但是,方法集在首次定义类时就已经固定了。在 Ruby 中,只有方法在类外部可见(必须使用“put”和“get”方法来访问字段),并且所有方法都必须明确声明。但是,可以修改现有的类声明,添加或覆盖方法。甚至可以逐个对象地执行此操作。因此,同一类的两个对象可能不会显示相同的行为。
Both Python and Ruby are more flexible than PHP or more traditional object-oriented languages regarding the contents (members) of a class. New fields can be added to a Python object simply by assigning to them: my_object.new_field = value. The set of methods, however, is fixed when the class is first defined. In Ruby only methods are visible outside a class (“put” and “get” methods must be used to access fields), and all methods must be explicitly declared. It is possible, however, to modify an existing class declaration, adding or overriding methods. One can even do this on an object-by-object basis. As a result, two objects of the same class may not display the same behavior.
Ruby 方法可以是public、protected或private。8 Python 中的访问控制纯粹是惯例问题;方法和字段都是普遍可访问的。最后,Python 具有多重继承。Ruby 具有混合继承:一个类不能从多个祖先获取数据。然而,与大多数其他语言不同,Ruby 允许接口(混合)不仅定义方法的签名,还定义其实现(代码)。
Ruby methods may be public, protected, or private.8 Access control in Python is purely a matter of convention; both methods and fields are universally accessible. Finally, Python has multiple inheritance. Ruby has mix-in inheritance: a class cannot obtain data from more than one ancestor. Unlike most other languages, however, Ruby allows an interface (mix-in) to define not only the signatures of methods but also their implementation (code).
脚本语言主要用于控制和协调其他软件组件。尽管它们的起源可以追溯到 20 世纪 60 年代的解释型语言,但多年来它们在很大程度上被学术计算机科学所忽视。然而,随着对程序员生产力的日益重视以及万维网的爆炸式增长,脚本语言在业界和学术界都越来越受到关注和欢迎。商业开发人员和开源社区已经取得了许多重大进展。脚本语言很可能会在 21 世纪主导编程,而传统的编译型语言则越来越多地被视为专用工具。
Scripting languages serve primarily to control and coordinate other software components. Though their roots go back to interpreted languages of the 1960s, for many years they were largely ignored by academic computer science. With an increasing emphasis on programmer productivity, however, and with the explosion of the World Wide Web, scripting languages have seen enormous growth in interest and popularity, both in industry and in academia. Many significant advances have been made by commercial developers and by the open-source community. Scripting languages may well come to dominate programming in the 21st century, with traditional compiled languages more and more seen as special-purpose tools.
与传统同类相比,脚本语言更注重灵活性和表达的丰富性,而不是纯粹的运行时性能。其共同特点包括批处理和交互式使用、表达的经济性、缺乏声明、简单的范围规则、灵活的动态类型、易于访问其他程序、复杂的模式匹配和字符串操作以及高级数据类型。
In comparison to their traditional cousins, scripting languages emphasize flexibility and richness of expression over sheer run-time performance. Common characteristics include both batch and interactive use, economy of expression, lack of declarations, simple scoping rules, flexible dynamic typing, easy access to other programs, sophisticated pattern matching and string manipulation, and high-level data types.
本章首先回顾了脚本的历史发展,从20 世纪 70 年代中期的命令解释器或shell程序开始,到随后出现的文本处理和报告生成工具。我们特别研究了“Bourne-again”shell、 bash以及 Unix 工具sed和awk。我们还提到了数学和统计学等特殊用途领域,在这些领域中,脚本语言被广泛用于数据分析、可视化、建模和模拟。然后,我们转向了当今脚本的三个主导领域:“粘合”(协调)应用程序、配置和扩展以及万维网脚本。
We began our chapter by tracing the historical development of scripting, starting with the command interpreter, or shell programs of the mid-1970s, and the text processing and report generation tools that followed soon thereafter. We looked in particular at the “Bourne-again” shell, bash, and the Unix tools sed and awk. We also mentioned such special-purpose domains as mathematics and statistics, where scripting languages are widely used for data analysis, visualization, modeling, and simulation. We then turned to the three domains that dominate scripting today: “glue” (coordination) applications, configuration and extension, and scripting of the World Wide Web.
多年来,Perl 一直是最受欢迎的通用“胶水”语言,但 Python 和 Ruby 显然已经超越了它。多种脚本语言(包括 Python、Scheme 和 Lua)被广泛用于扩展复杂应用程序的功能。此外,许多商业软件包都有自己的专有扩展语言。
For many years, Perl was the most popular of the general-purpose “glue” languages, but Python and Ruby have clearly overtaken it at this point. Several scripting languages, including Python, Scheme, and Lua, are widely used to extend the functionality of complex applications. In addition, many commercial packages have their own proprietary extension languages.
Web 脚本有多种形式。在 HTTP 连接的服务器端,通用网关接口 (CGI) 标准允许使用 URI 来命名将用于生成动态内容的程序。或者,可以使用通常以 PHP 编写的网页嵌入脚本以用户不可见的方式创建动态内容。为了减少服务器负载并提高交互响应能力,脚本也可以在客户端浏览器中执行。JavaScript 是此领域的主要符号;它使用 HTML 文档对象模型 (DOM) 来操作网页元素。对于更苛刻的任务,可以指示许多浏览器运行 Java 小程序,该小程序将完全负责部分“屏幕空间”,但这种策略会带来安全问题,人们越来越认为这种策略是不可接受的。随着 HTML5 的出现,大多数动态内容(尤其是多媒体)都可以由浏览器直接处理。同时,XML 已成为结构化、与演示无关的信息的标准格式,可通过 XSL 进行加载时转换。
Web scripting comes in many forms. On the server side of an HTTP connection, the Common Gateway Interface (CGI) standard allows a URI to name a program that will be used to generate dynamic content. Alternatively, web-page-embedded scripts, often written in PHP, can be used to create dynamic content in a way that is invisible to users. To reduce the load on servers, and to improve interactive responsiveness, scripts can also be executed within the client browser. JavaScript is the dominant notation in this domain; it uses the HTML Document Object Model (DOM) to manipulate web-page elements. For more demanding tasks, many browsers can be directed to run a Java applet, which takes full responsibility for some portion of the “screen real estate,” but this strategy comes with security concerns that are increasingly viewed as unacceptable. With the emergence of HTML5, most dynamic content—multimedia in particular—can be handled directly by the browser. At the same time, XML has emerged as the standard format for structured, presentation-independent information, with load-time transformation via XSL.
由于脚本语言发展迅速,它们能够利用前面章节中介绍的许多最强大、最优雅的机制,包括一等函数和高阶函数、无限范围、迭代器、垃圾收集、列表理解和面向对象,更不用说扩展正则表达式和字典、集合和元组等高级数据类型。鉴于目前的趋势,脚本语言可能会变得越来越普遍,并继续成为语言创新的主要焦点。
Because of their rapid evolution, scripting languages have been able to take advantage of many of the most powerful and elegant mechanisms described in previous chapters, including first-class and higher-order functions, unlimited extent, iterators, garbage collection, list comprehensions, and object orientation—not to mention extended regular expressions and such high-level data types as dictionaries, sets, and tuples. Given current trends, scripting languages are likely to become increasingly ubiquitous, and to remain a principal focus of language innovation.
14.1 文件名“通配符”是否提供了标准正则表达式的表达能力?解释一下。
14.1 Does filename “globbing” provide the expressive power of standard regular expressions? Explain.
14.2 编写 shell 脚本
14.2 Write shell scripts to
(a) Replace blanks with underscores in the names of all files in the current directory.
(b) 通过在文件名称前面添加修改日期的文本表示来重命名当前目录下的每个文件。
(b) Rename every file in the current directory by prepending to its name a textual representation of its modification date.
(c) 在当前目录下的文件层次结构中查找所有eps文件,并创建任何缺失或过时的相应pdf文件。
(c) Find all eps files in the file hierarchy below the current directory, and create any corresponding pdf files that are missing or out of date.
(d) 打印当前目录下文件层次结构中给定谓词求值为真值的所有文件的名称。您的(引用的)谓词应在命令行中使用 Unix test命令的语法指定,其中一个或多个@符号代表候选文件的名称。
(d) Print the names of all files in the file hierarchy below the current directory for which a given predicate evaluates to true. Your (quoted) predicate should be specified on the command line using the syntax of the Unix test command, with one or more at signs (@) standing in for the name of the candidate file.
14.3 在示例 14.16中,我们使用“$@”来引用传递给ll 的参数。如果我们删除引号会发生什么?(提示:对名称包含空格的文件尝试此操作!)阅读bash的手册页并了解$@和$*之间的区别。创建使用$*或“$*”而不是“$@”的ll版本。解释发生了什么。
14.3 In Example 14.16 we used “$@” to refer to the parameters passed to ll. What would happen if we removed the quote marks? (Hint: Try this for files whose names contain spaces!) Read the man page for bash and learn the difference between $@ and $*. Create versions of ll that use $* or “$*” instead of “$@”. Explain what's going on.
14.4
14.4
(a)扩展 图 14.5、14.6或14.7中的代码,尝试更温和地终止进程。您需要阅读标准kill命令的手册页。首先使用TERM信号。如果不起作用,请询问用户是否应该求助于KILL。
(a) Extend the code in Figure 14.5, 14.6, or 14.7 to try to kill processes more gently. You'll want to read the man page for the standard kill command. Use a TERM signal first. If that doesn't work, ask the user if you should resort to KILL.
(b) 将您的解决方案扩展到部分 (a),以便脚本接受指定要使用的信号的可选参数。TERM和KILL的替代方案包括HUP、INT、QUIT和ABRT。
(b) Extend your solution to part (a) so that the script accepts an optional argument specifying the signal to be used. Alternatives to TERM and KILL include HUP, INT, QUIT, and ABRT.
14.5 编写一个 Perl、Python 或 Ruby 脚本来创建一个简单的索引:一个按顺序排列的出现在输入文档中的重要单词列表,每个单词都有一个子列表,指示该单词出现的行,以及最多六个上下文单词。从列表中排除所有常用冠词、连词、介词和代词。
14.5 Write a Perl, Python, or Ruby script that creates a simple concordance: a sorted list of significant words appearing in an input document, with a sublist for each that indicates the lines on which the word occurs, with up to six words of surrounding context. Exclude from your list all common articles, conjunctions, prepositions, and pronouns.
14.6 编写 Emacs Lisp 脚本
14.6 Write Emacs Lisp scripts to
(a) 将今天的日期插入到当前缓冲区的插入点(当前光标位置)。
(a) Insert today's date into the current buffer at the insertion point (current cursor location).
(b) 在插入点周围的单词上加上引号(“ “)。
(b) Place quote marks (“ “) around the word surrounding the insertion point.
(c) 修复当前缓冲区中的句末空格。使用以下启发式方法:如果句号、问号或感叹号后面跟着一个空格(中间可能有右引号、圆括号、方括号或花括号),则添加一个额外的空格,除非句号、问号或感叹号前面的字符是大写字母(在这种情况下我们假设它是缩写)。
(c) Fix end-of-sentence spaces in the current buffer. Use the following heuristic: if a period, question mark, or exclamation point is followed by a single space (possibly with closing quote marks, parentheses, brackets, or braces in-between), then add an extra space, unless the character preceding the period, question mark, or exclamation point is a capital letter (in which case we assume it is an abbreviation).
(d) 通过您最喜欢的拼写检查器运行当前缓冲区的内容,并创建一个包含拼写错误单词列表的新缓冲区。
(d) Run the contents of the current buffer through your favorite spell checker, and create a new buffer containing a list of misspelled words.
(e) 从(d)中创建的缓冲区中删除一个拼写错误的单词,并将光标(插入点)放在当前缓冲区中该拼写错误的单词第一次出现的上方。
(e) Delete one misspelled word from the buffer created in (d), and place the cursor (insertion point) on top of the first occurrence of that misspelled word in the current buffer.
14.7 解释在什么情况下将 Web 上的交互任务实现为 CGI 脚本、嵌入式服务器端脚本或客户端脚本是有意义的。对于每种实现选择,给出三个明显是首选方法的任务示例。
14.7 Explain the circumstances under which it makes sense to realize an interactive task on the Web as a CGI script, an embedded server-side script, or a client-side script. For each of these implementation choices, give three examples of tasks for which it is clearly the preferred approach.
14.8
14.8
(a) 编写一个嵌入 PHP 的网页,打印帕斯卡三角形的前 10 行(如果您不知道这是什么,请参阅示例 C-17.10)。渲染后,您的输出应如图14.21所示。
(a) Write a web page with embedded PHP to print the first 10 rows of Pascal's triangle (see Example C-17.10 if you don't know what this is). When rendered, your output should look like Figure 14.21.
(b) 修改您的页面以创建一个自发布表单,该表单接受输入字段中所需的行数。
(b) Modify your page to create a self-posting form that accepts the number of desired rows in an input field.
(c) 用 JavaScript 重写您的页面。
(c) Rewrite your page in JavaScript.
14.9 创建一个填写式 Web 表单,使用 Luhn 公式的 JavaScript 实现(练习 4.10)来检查信用卡号中的拼写错误。(但不要使用真实的信用卡号;家庭作业练习往往不太安全!)
14.9 Create a fill-in web form that uses a JavaScript implementation of the Luhn formula (Exercise 4.10) to check for typos in credit card numbers. (But don't use real credit card numbers; homework exercises don't tend to be very secure!)
14.10
14.10
(a)修改 图 14.15的代码(示例 14.35),使其用输出替换表单,就像图 14.11和14.14的 CGI 和 PHP 版本所做的那样。
(a) Modify the code of Figure 14.15 (Example 14.35) so that it replaces the form with its output, as the CGI and PHP versions of Figures 14.11 and 14.14 do.
(b)修改 图 14.11和14.14(示例 14.30和14.34 )的 CGI 和 PHP 脚本,使它们看起来将其输出附加到表单的底部,就像图 14.15的 JavaScript 版本一样。
(b) Modify the CGI and PHP scripts of Figures 14.11 and 14.14 (Examples 14.30 and 14.34) so they appear to append their output to the bottom of the form, as the JavaScript version of Figure 14.15 does.
14.11 在 Perl 中运行以下程序:sub foo { my $lex = $_[0]; sub bar { print “$lex\n”; } bar(); } foo(2); foo(3);您可能会对输出感到惊讶。Perl 5 允许命名子例程嵌套,但不能正确地为它们创建闭包。重写上述代码以创建对匿名本地子例程的引用,并验证它是否正确创建了闭包。将 use diagnostics ;行添加到原始版本的开头并再次运行它。根据这将给您的解释,推测嵌套命名子例程在 Perl 5 中是如何实现的。
14.11 Run the following program in Perl:
sub foo {
my $lex = $_[0];
sub bar {
print “$lex\n”;
}
bar();
}
foo(2); foo(3);
You may be surprised by the output. Perl 5 allows named subroutines to nest, but does not create closures for them properly. Rewrite the code above to create a reference to an anonymous local subroutine and verify that it does create closures correctly. Add the line use diagnostics; to the beginning of the original version and run it again. Based on the explanation this will give you, speculate as to how nested named subroutines are implemented in Perl 5.
14.12 编写一个程序,映射当前目录下文件层次结构中存储的网页。输出本身应为一个网页,包含所有目录和.html文件的名称,并以与文件层次结构中的级别相对应的缩进级别打印。每个.html文件名应为其文件的实际链接。使用最适合该任务的任何语言。
14.12 Write a program that will map the web pages stored in the file hierarchy below the current directory. Your output should itself be a web page, containing the names of all directories and .html files, printed at levels of indentation corresponding to their level in the file hierarchy. Each .html file name should be a live link to its file. Use whatever language(s) seem most appropriate to the task.
14.13 在14.4.1 节中,我们声称 Ruby 中的嵌套块是它们出现的命名范围的一部分。通过运行以下 Ruby 脚本并解释其输出来验证此声明:def foo(x) y = 2 bar = proc { print x, “\n” y = 3 } bar.call() print y, “\n” end foo(3)现在注释掉第二行(y = 2)并再次运行脚本。解释发生了什么。更仔细、更准确地重述我们关于范围的声明。
14.13 In Section 14.4.1 we claimed that nested blocks in Ruby were part of the named scope in which they appear. Verify this claim by running the following Ruby script and explaining its output:
def foo(x)
y = 2
bar = proc {
print x, “\n”
y = 3
}
bar.call()
print y, “\n”
end
foo(3)
Now comment out the second line (y = 2) and run the script again. Explain what happens. Restate our claim about scoping more carefully and precisely.
14.14 编写一个 Perl 脚本,将英制测量单位(in、ft、yd、mi)转换为公制单位(cm、m、km)。您可能想要了解正则表达式中的e修饰符,它允许s///e表达式的右侧包含可执行代码。
14.14 Write a Perl script to translate English measurements (in, ft, yd, mi) into metric equivalents (cm, m, km). You may want to learn about the e modifier on regular expressions, which allows the right-hand side of an s///e expression to contain executable code.
14.15 编写一个 Perl 脚本,针对每个输入行,找出在该行中至少出现两次且不重叠的最长子字符串。(警告:这比听起来要难。请记住,默认情况下,Perl 会搜索最左边的最长匹配。)
14.15 Write a Perl script to find, for each input line, the longest substring that appears at least twice within the line, without overlapping. (Warning: This is harder than it sounds. Remember that by default Perl searches for a left-most longest match.)
14.16 Perl 提供了一种替代形式的括号(?:…),它支持在正则表达式中分组而不执行捕获。使用此语法,示例 14.57可以编写如下:if (/^([+-]?)((\d+)\.|(\d*)\.(\d+))(?:e([+-]?\d+))?$/) { # 浮点数print “sign: “, $1, “\n”; print “integer: “, $3, $4, “\n”; print “fraction: “, $5, “\n”; print “mantissa: “, $2, “\n”; print “exponent: “, $6, “\n”; # not $7 }这种额外的符号有什么用途?为什么这里的代码比示例 14.57的代码更可取?
14.16 Perl provides an alternative (?:…) form of parentheses that supports grouping in regular expressions without performing capture. Using this syntax, Example 14.57 could have been written as follows:
if (/^([+-]?)((\d+)\.|(\d*)\.(\d+))(?:e([+-]?\d+))?$/) {
# floating-point number
print “sign: “, $1, “\n”;
print “integer: “, $3, $4, “\n”;
print “fraction: “, $5, “\n”;
print “mantissa: “, $2, “\n”;
print “exponent: “, $6, “\n”; # not $7
}
What purpose does this extra notation serve? Why might the code here be preferable to that of Example 14.57?
14.17 再考虑一下图 14.1中的sed代码。我们很容易将第一个复合语句写成如下形式(注意三个替换命令的区别):/<[hH][123]>.*<\/[hH][123]>/ { ;# 匹配整个标题h ;# 保存模式空间的副本s/^.*\(<[hH][123]>\)/\1/ ;# 删除开始标记之前的文本s/\(<\/[hH][123]>\).*$/\1/ ;# 删除结束标记之后的文本p ;# 打印剩余内容g ;# 检索保存的模式空间s/^.*<\/[hH][123]>// ;# 删除结束标记之前的内容b top解释为什么这样做不起作用。(提示:记住贪婪匹配和最小匹配之间的区别 [示例 14.53 ]。Sed缺少后者。)
14.17 Consider again the sed code of Figure 14.1. It is temptingto write the first of the compound statements as follows (note the differences in the three substitution commands):
/<[hH][123]>.*<\/[hH][123]>/ { ;# match whole heading
h ;# save copy of pattern space
s/^.*\(<[hH][123]>\)/\1/ ;# delete text before opening tag
s/\(<\/[hH][123]>\).*$/\1/ ;# delete text after closing tag
p ;# print what remains
g ;# retrieve saved pattern space
s/^.*<\/[hH][123]>// ;# delete through closing tag
b top
Explain why this doesn't work. (Hint: Remember the difference between greedy and minimal matches [Example 14.53]. Sed lacks the latter.)
14.18 考虑 Perl 中的以下正则表达式:/^(?:((?:ab)+) |a((?:ba)*))$/。用英语描述它将匹配的字符串集。显示该集合的自然 NFA 以及最小 DFA。描述每个匹配字符串中应捕获的子字符串。基于此示例,讨论在 Perl 中使用 DFA 匹配字符串的实用性。
14.18 Consider the following regular expression in Perl: /^(?:((?:ab)+) |a((?:ba)*))$/. Describe, in English, the set of strings it will match. Show a natural NFA for this set, together with the minimal DFA. Describe the substrings that should be captured in each matching string. Based on this example, discuss the practicality of using DFAs to match strings in Perl.
14.19–14.21 更深入。
14.19–14.21 In More Depth.
14.22 了解本书使用的排版系统 T E X [ Knu86 ] 和 L A T E X [ Lam94 ]。探索其专门针对目标的方式领域——专业排版——影响了它的设计。您可能希望考虑的功能包括动态作用域、相对贫乏的算术和控制流功能以及使用宏作为基本控制抽象。
14.22 Learn about TEX [Knu86] and LATEX [Lam94], the typesetting system used to create this book. Explore the ways in which its specialized target domain—professional typesetting—influenced its design. Features you might wish to consider include dynamic scoping, the relatively impoverished arithmetic and control-flow facilities, and the use of macros as the fundamental control abstraction.
14.23 研究 JavaScript 和/或 Java 小程序的安全机制。程序到底可以做什么以及为什么?哪些可能有用的功能由于无法保证安全而未提供?提供的功能中还存在哪些潜在的安全漏洞?
14.23 Research the security mechanisms of JavaScript and/or Java applets. What exactly are programs allowed to do and why? What potentially useful features have not been provided because they can't be made secure? What potential security holes remain in the features that are provided?
14.24 了解网络爬虫— 探索万维网的程序。编写一个搜索您感兴趣内容的爬虫。哪些语言特性或工具似乎对这项任务最有用?警告:自动网络爬虫是一项公共活动,受严格的礼仪规则约束。在创建爬虫之前,请先进行网络搜索并了解规则,并在将其放在本地子网(甚至您自己的机器)之外之前非常仔细地测试您的代码。特别要注意,对同一台服务器的快速请求构成拒绝服务攻击,这是一种潜在的刑事犯罪。
14.24 Learn about web crawlers—programs that explore the World Wide Web. Build a crawler that searches for something of interest. What language features or tools seem most useful for the task? Warning: Automated web crawling is a public activity, subject to strict rules of etiquette. Before creating a crawler, do a web search and learn the rules, and test your code very carefully before letting it outside your local subnet (or even your own machine). In particular, be aware that rapid-fire requests to the same server constitute a denial of service attack, a potentially criminal offense.
14.25 在侧边栏 14.9 中,我们指出awk和egrep的“扩展”正则表达式通常通过先转换为 NFA,然后再转换为 DFA 来实现,而 Perl 及其同类的正则表达式通常通过回溯搜索来实现。一些工具(包括 GNU ggrep)使用 Boyer-Moore-Gosper 算法 [ BM77、KMP77 ] 的变体来实现更快的确定性搜索。了解此算法的工作原理。它的优点是什么?它可以用于 Perl 等语言吗?
14.25 In Sidebar 14.9 we noted that the “extended” REs of awk and egrep are typically implemented by translating first to an NFA and then to a DFA, while those of Perl and its ilk are typically implemented via backtracking search. Some tools, including GNU ggrep, use a variant of the Boyer-Moore-Gosper algorithm [BM77, KMP77] for faster deterministic search. Find out how this algorithm works. What are its advantages? Could it be used in languages like Perl?
14.26 在侧边栏 14.10 中,我们指出,非常量模式通常必须在使用时重新编译。希望减少由此产生的开销的 Perl 程序员可以使用o尾随修饰符或qr引用运算符来禁止重新编译。研究这些机制对性能的影响。同时推测语言实现在多大程度上可以自动高效地确定何时应该进行重新编译。
14.26 In Sidebar 14.10 we noted that nonconstant patterns must generally be recompiled whenever they are used. Perl programmers who wish to reduce the resulting overhead can inhibit recompilation using the o trailing modifier or the qr quoting operator. Investigate the impact of these mechanisms on performance. Also speculate as to the extent to which it might be possible for the language implementation to determine, automatically and efficiently, when recompilation should occur.
14.27在 第 14.4.2 节中,我们对 Perl RE 的介绍并不完整。未介绍的功能包括前瞻和后瞻(上下文)断言、注释、修饰符的增量启用和禁用、嵌入代码、条件、Unicode 支持、非斜线分隔符和音译(tr///)运算符。了解这些功能的工作原理。解释它们是否(以及如何)扩展了符号的表达能力。如果没有这些功能,如何模拟它们(可能使用周围的 Perl 代码)?
14.27 Our coverage of Perl REs in Section 14.4.2 was incomplete. Features not covered include look-ahead and look-behind (context) assertions, comments, incremental enabling and disabling of modifiers, embedded code, conditionals, Unicode support, nonslash delimiters, and the transliteration (tr///) operator. Learn how these work. Explain if (and how) they extend the expressive power of the notation. How could each be emulated (possibly with surrounding Perl code) if it were not available?
14.28 研究 PHP、Tcl、Python、Ruby、JavaScript、Emacs Lisp、Java 和 C# 中 RE 支持的细节。写一篇论文,尽可能简洁地记录它们之间的差异,并使用 Perl 作为比较的参考。
14.28 Investigate the details of RE support in PHP, Tcl, Python, Ruby, JavaScript, Emacs Lisp, Java, and C#. Write a paper that documents, as concisely as possible, the differences among these, using Perl as a reference for comparison.
14.29 在网络上搜索 Perl 6,这是一项基于社区的努力,已经进行了多年。写一份报告总结变化,并附上尊重 Perl 5。您如何看待这些变化?如果您负责修订,您会做哪些不同的事情?
14.29 Do a web search for Perl 6, a community-based effort that has been in the works for many years. Write a report that summarizes the changes with respect to Perl 5. What do you think of these changes? If you were in charge of the revision, what would you do differently?
14.30 了解 AJAX,这是一组网络技术,允许 JavaScript 程序在“后台”与网络服务器交互,以便在初始加载后动态更新浏览器中的页面。您可以使用 AJAX 构建哪些类型的应用程序,而这些应用程序您无法轻松构建?JavaScript 的哪些功能对于使该技术发挥作用最重要?
14.30 Learn about AJAX, a collection ofweb technologies that allows a JavaScript program to interact with web servers “in the background,” to dynamically update a page in a browser after its initial loading. What kinds of applications can you build in AJAX that you couldn't easily build otherwise? What features of JavaScript are most important for making the technology work?
14.31 了解 Google 开发的一种语言 Dart。Dart 最初旨在作为 JavaScript 的后继者,现在仅支持将其作为开发将翻译成JavaScript 的代码的语言。如何解释这种策略的改变?未来几年 JavaScript 出现其他竞争对手的可能性有多大?
14.31 Learn about Dart, a language developed at Google. Initially intended as a successor to JavaScript, Dart is now supported only as a language in which to develop code that will be translated into JavaScript. What explains the change in strategy? What are the odds that some other competitor to JavaScript will emerge in future years?
14.32–14.35 更深入。
14.32–14.35 In More Depth.
大多数主流脚本语言及其前身都在语言设计者或其亲密同事的书中进行了描述:awk [ AKW88 ]、Perl [ CfWO12 ]、PHP [ TML13 ]、Python [ vRD11 ] 和 Ruby [ TFH13 ]。其中几本书有在线版本。大多数语言也在各种其他文本中进行了描述,并且大多数都有专门的网站:perl.org、php.net、python.org、ruby-lang.org。许多机器预装了有关 Perl 的大量文档;输入man perl可查看索引。
Most of the major scripting languages and their predecessors are described in books by the language designers or their close associates: awk [AKW88], Perl [CfWO12], PHP [TML13], Python [vRD11], and Ruby [TFH13]. Several of these books have versions available on-line. Most of the languages are also described in a variety of other texts, and most have dedicated web sites: perl.org, php.net, python.org, ruby-lang.org. Extensive documentation for Perl is pre-installed on many machines; type man perl for an index.
Rexx [ Ame96a ] 由美国国家标准协会 (ANSI) 标准化。JavaScript [ ECM11 ] 由欧洲标准机构 ECMA 标准化。Guile ( gnu.org/software/guile/ ) 是 GNU 的 Scheme 脚本实现。万维网标准(包括 HTML5、XML、XSL、XPath 和 XSLT)由万维网联盟 ( www.w3.org ) 颁布。对于那些将页面更新为 HTML5 的用户来说, validator.w3.org上的验证服务特别有用。w3schools.com 上有许多与 Web 相关的主题的高质量教程。
Rexx [Ame96a] was standardized by ANSI, the American National Standards Institute. JavaScript [ECM11] is standardized by ECMA, the European standards body. Guile (gnu.org/software/guile/) is GNU's Scheme implementation for scripting. Standards for the World Wide Web, including HTML5, XML, XSL, XPath, and XSLT, are promulgated by the World Wide Web Consortium: www.w3.org. For those updating their pages to HTML5, the validation service at validator.w3.org is particularly useful. High-quality tutorials on many web-related topics can be found at w3schools.com.
Hauben 和 Hauben [ HH97a ] 描述了互联网的历史根源,包括早期的 Unix 工作。关于各种 Unix shell 语言的原始文章包括 Mashey [ Mas76 ]、Bourne [ Bou78 ] 和 Korn [ Kor94 ] 的文章。关于 APL 的原始参考资料来自 Iverson [ Ive62 ]。Ousterhout [ Ous98 ] 介绍了脚本语言的一般情况,特别是 Tcl。Chonacky 和 Winch [ CW05 ] 对 Maple、Mathematica 和 Matlab 进行了比较和对比。Richard Gabriel 的“Worse Is Better”论文集可以在www.dreamsongs.com/WorseIsBetter.html上找到。在 Abelson、Greenspun 和 Sandon 的在线Tcl for Web Nerds指南(philip.greenspun.com/tcl/index.adp)的介绍章节中可以找到类似的 Tcl 和 Scheme 的比较。
Hauben and Hauben [HH97a] describe the historical roots of the Internet, including early work on Unix. Original articles on the various Unix shell languages include those of Mashey [Mas76], Bourne [Bou78], and Korn [Kor94]. The original reference on APL is by Iverson [Ive62]. Ousterhout [Ous98] makes the case for scripting languages in general, and Tcl in particular. Chonacky and Winch [CW05] compare and contrast Maple, Mathematica, and Matlab. Richard Gabriel's collection of “Worse Is Better” papers can be found at www.dreamsongs.com/WorseIsBetter.html. A similar comparison of Tcl and Scheme can be found in the introductory chapter of Abelson, Greenspun, and Sandon's on-line Tcl for Web Nerds guide (philip.greenspun.com/tcl/index.adp).
进一步了解实现情况
A Closer Look at Implementation
在此,即本文的最后且最短的部分中,我们将焦点重新转移到实现问题上。
In this, the final and shortest of the major parts of the text, we return our focus to implementation issues.
第十五章 考虑了在语义分析之后必须完成的工作,以生成可运行的程序。本章的前半部分概括地描述了典型编译器的后端结构,调查了中间程序表示,并使用第 4 章的属性语法框架来描述编译器如何生成汇编级代码。本章的后半部分描述了典型进程地址空间的结构,并解释了汇编器和链接器如何将编译器的输出转换为可执行代码。
Chapter 15 considers the work that must be done, in the wake of semantic analysis, to generate a runnable program. The first half of the chapter describes, in general terms, the structure of the back end of the typical compiler, surveys intermediate program representations, and uses the attribute grammar framework of Chapter 4 to describe how a compiler produces assembly-level code. The second halfofthe chapter describes the structure ofthe typical process address space, and explains how the assembler and linker transform the output of the compiler into executable code.
在任何非平凡的语言实现中,编译器都假设存在大量预先存在的代码,用于存储管理、异常处理、动态链接等。更复杂的语言可能还需要事件、线程和消息。当实现这些功能的库依赖于编译器的知识或正在运行的程序的结构时,它们就构成了运行时系统。我们将在第 16 章中讨论此类系统。我们特别关注虚拟机;机器代码的运行时操作;以及反射机制,这些机制允许程序推断其运行时结构和类型。
In any nontrivial language implementation, the compiler assumes the existence of a large body of preexisting code for storage management, exception handling, dynamic linking, and the like. A more sophisticated language may require events, threads, and messages as well. When the libraries that implement these features depend on knowledge of the compiler or of the structure of the running program, they are said to constitute a run-time system. We consider such systems in Chapter 16. We focus in particular on virtual machines; run-time manipulation of machine code; and reflection mechanisms, which allow a program to reason about its runtime structure and types.
第 15 章中的后端编译器描述必然过于简单。整本书和课程都致力于讲述更完整的故事,其中大部分都侧重于用于生成高效代码的代码改进或优化技术。当前文本的第 17 章完全包含在配套网站上,概述了代码改进。由于大多数程序员永远不会编写编译器的后端,因此第 17 章的目标更多的是传达编译器的功能,而不是它如何执行。理解这些材料的程序员将能够更好地“使用”编译器,了解什么是可能的,在常见情况下会发生什么,以及如何避免难以优化的编程习语。主题包括本地和“全局”(过程级)冗余消除、数据流分析、循环优化和寄存器分配。
The back-end compiler description in Chapter 15 is by necessity simplistic. Entire books and courses are devoted to the fuller story, most of which focuses on the code improvement or optimization techniques used to produce efficient code. Chapter 17 of the current text, contained entirely on the companion site, provides an overview of code improvement. Since most programmers will never write the back end of a compiler, the goal of Chapter 17 is more to convey a sense of what the compiler does than exactly how it does it. Programmers who understand this material will be in a better position to “work with” the compiler, knowing what is possible, what to expect in common cases, and how to avoid programming idioms that are hard to optimize. Topics include local and “global” (procedure-level) redundancy elimination, data flow analysis, loop optimization, and register allocation.
如第 1.6 节所述,编译的各个阶段通常分为负责源代码分析的前端、负责目标代码合成的后端,以及通常负责独立于语言和机器的代码改进的“中端”。第 2 章和第4章讨论了前端的工作,最终构建了语法树。本章将介绍后端的工作,特别是代码生成、汇编和链接。我们将在第17 章继续讨论代码改进。
As noted in Section 1.6, the various phases of compilation are commonly grouped into a front end responsible for the analysis of source code, a back end responsible for the synthesis of target code, and often a “middle end” responsible for language- and machine-independent code improvement. Chapters 2 and 4 discussed the work of the front end, culminating in the construction of a syntax tree. The current chapter turns to the work of the back end, and specifically to code generation, assembly, and linking. We will continue with code improvement in Chapter 17.
在第 6 章到第 10章中,我们经常讨论编译器为实现各种语言特性而生成的代码。现在我们将看看编译器如何根据语法树生成代码,以及如何将多次编译的输出组合起来生成可运行的程序。从第15.1 节开始,我们将比第 1 章更详细地概述程序合成的工作。我们特别关注将这项工作划分为阶段的几种合理方法中的一种。在第 15.2 节中,我们将考虑在这些阶段之间传递的中间代码的许多可能形式。在配套站点上,我们提供了两个具体示例的更详细信息——GNU 编译器使用的 GIMPLE 和 RTL 格式。我们将在第16 章中考虑另外两种中间形式:Java 字节码和 Microsoft 及公共语言基础结构的其他实现者使用的公共中间语言 (CIL)。
In Chapters 6 through 10, we often discussed the code that a compiler would generate to implement various language features. Now we will look at how the compiler produces that code from a syntax tree, and how it combines the output of multiple compilations to produce a runnable program. We begin in Section 15.1 with a more detailed overview of the work of program synthesis than was possible in Chapter 1. We focus in particular on one of several plausible ways of dividing that work into phases. In Section 15.2 we then consider the many possible forms of intermediate code passed between these phases. On the companion site we provide a bit more detail on two concrete examples—the GIMPLE and RTL formats used by the GNU compilers. We will consider two additional intermediate forms in Chapter 16: Java bytecode and the Common Intermediate Language (CIL) used by Microsoft and other implementors of the Common Language Infrastructure.
在15.3 节中,我们讨论如何使用属性文法作为形式框架,从抽象语法树生成汇编代码。在15.4 节中,我们讨论二进制目标文件的内部组织和程序在内存中的布局。在15.5 节中,我们描述汇编。在 15.6 节中,我们讨论链接。
In Section 15.3 we discuss the generation of assembly code from an abstract syntax tree, using attribute grammars as a formal framework. In Section 15.4 we discuss the internal organization of binary object files and the layout of programs in memory. Section 15.5 describes assembly. Section 15.6 considers linking.
正如我们在第 4 章中提到的那样,后端编译器结构的统一性不如前端结构。即使是文本这样的非常规编译器处理器、源到源翻译器和 VLSI 布局工具必须扫描、解析和分析其输入的语义。然而,当涉及到后端时,即使是同一台机器上同一种语言的编译器也可能具有非常不同的内部结构。
As we noted in Chapter 4, there is less uniformity in back-end compiler structure than there is in front-end structure. Even such unconventional compilers as text processors, source-to-source translators, and VLSI layout tools must scan, parse, and analyze the semantics of their input. When it comes to the back end, however, even compilers for the same language on the same machine can have very different internal structure.
正如我们将在15.2 节中看到的,不同的编译器可能使用不同的中间形式在内部表示程序。它们在执行的代码改进形式上也可能有很大差异。简单的编译器或为编译速度而不是目标代码执行速度而设计的编译器(例如,“即时”编译器)可能根本不会做太多的改进。即时或“即装即用”编译器(将程序作为单个高级操作编译然后执行,而不将目标代码写入文件的编译器)可能不使用单独的链接器。在某些编译器中,大部分或全部代码生成器可能由以目标机器的正式描述作为输入的工具(“代码生成器”)自动编写。
As we shall see in Section 15.2, different compilers may use different intermediate forms to represent a program internally. They may also differ dramatically in the forms of code improvement they perform. A simple compiler, or one designed for speed of compilation rather than speed of target code execution (e.g., a “just-in-time” compiler) may not do much improvement at all. A just-in-time or “load-and-go” compiler (one that compiles and then executes a program as a single high-level operation, without writing the target code to a file) may not use a separate linker. In some compilers, much or all of the code generator may be written automatically by a tool (a “code generator generator”) that takes a formal description of the target machine as input.
虽然某些代码改进可以在语法树上执行,但程序的层次结构越少,大多数代码改进就越容易。因此,我们的示例编译器包含一个用于中间代码生成的显式阶段。代码生成器首先将树的节点分组为基本块,每个基本块都包含一组最大长度的操作,这些操作应在运行时按顺序执行,而没有进出分支。然后,它创建一个控制流图,其中节点是基本块,弧表示块间控制流。在每个基本块中,操作表示为具有无限数量寄存器的理想机器的指令。我们将这些称为虚拟寄存器。通过为每个计算值分配一个新的寄存器,编译器可以避免在编译过程的早期在原本独立的计算之间建立人为连接。
While certain code improvements can be performed on syntax trees, a less hierarchical representation of the program makes most code improvement easier. Our example compiler therefore includes an explicit phase for intermediate code generation. The code generator begins by grouping the nodes of the tree into basic blocks, each of which consists of a maximal-length set of operations that should execute sequentially at run time, with no branches in or out. It then creates a control flow graph in which the nodes are basic blocks and the arcs represent interblock control flow. Within each basic block, operations are represented as instructions for an idealized machine with an unlimited number of registers. We will call these virtual registers. By allocating a new one for every computed value, the compiler can avoid creating artificial connections between otherwise independent computations too early in the compilation process.
编译的与机器无关的代码改进阶段对控制流图执行各种转换。它修改每个基本块内的指令序列以消除冗余的加载、存储和算术计算;这是局部代码改进。它还识别并删除子程序内基本块之间边界上的各种冗余;这是全局代码改进。作为后者的一个例子,在if语句之前立即计算的表达式的值不需要在else之后的代码中重新计算。同样,如果出现在循环体内的表达式的值在后续迭代中不会改变,则只需求值一次。一些全局改进会改变基本块的数量和/或它们之间的弧。
The machine-independent code improvement phase of compilation performs a variety of transformations on the control flow graph. It modifies the instruction sequence within each basic block to eliminate redundant loads, stores, and arithmetic computations; this is local code improvement. It also identifies and removes a variety of redundancies across the boundaries between basic blocks within a subroutine; this is global code improvement. As an example of the latter, an expression whose value is computed immediately before an if statement need not be recomputed within the code that follows the else. Likewise an expression that appears within the body of a loop need only be evaluated once if its value will not change in subsequent iterations. Some global improvements change the number of basic blocks and/or the arcs among them.
值得注意的是,“全局”代码改进通常只考虑当前子程序,而不是整个程序。编译器技术的许多最新研究都针对“真正的全局”技术,称为过程间代码改进。由于程序员一般不愿意放弃单独编译(重新编译数十万行代码是一项非常耗时的操作),所以实用的过程间代码改进器必须在链接时完成大部分工作。要克服的(众多)挑战之一是开发一种分工和中间表示,使编译器能够在(单独)编译期间完成尽可能多的工作,但留下足够多的细节未确定,以便链接时代码改进器能够完成其工作。
It is worth noting that “global” code improvement typically considers only the current subroutine, not the program as a whole. Much recent research in compiler technology has been aimed at “truly global” techniques, known as interprocedural code improvement. Since programmers are generally unwilling to give up separate compilation (recompiling hundreds of thousands of lines of code is a very time-consuming operation), a practical interprocedural code improver must do much of its work at link time. One of the (many) challenges to be overcome is to develop a division of labor and an intermediate representation that allow the compiler to do as much work as possible during (separate) compilation, but leave enough of the details undecided that the link-time code improver is able to do its job.
在完成独立于机器的代码改进之后,编译的下一个阶段是目标代码生成。此阶段将基本块串在一起形成一个线性程序,将每个块转换为目标机器的指令集,并生成与控制流图弧相对应的分支指令(或“fall-through”)。此阶段的输出与实际汇编语言的主要区别在于它继续依赖虚拟寄存器。只要中间形式的伪指令与目标机器的伪指令相当接近,这个编译阶段虽然很繁琐,但或多或少还是很简单的。
Following machine-independent code improvement, the next phase of compilation is target code generation. This phase strings the basic blocks together into a linear program, translating each block into the instruction set of the target machine and generating branch instructions (or “fall-throughs”) that correspond to the arcs of the control flow graph. The output of this phase differs from real assembly language primarily in its continued reliance on virtual registers. So long as the pseudoinstructions of the intermediate form are reasonably close to those of the target machine, this phase of compilation, though tedious, is more or less straightforward.
为了减少程序员的工作量并提高将编译器移植到新目标机器的难度,有时会根据机器的正式描述自动生成目标代码生成器。自动生成的代码生成器都依赖于某种模式匹配算法,用等效的目标机器指令序列替换中间代码指令序列。本章末尾的参考书目注释中可以找到对几种此类算法的引用;详细信息超出了本书的范围。
To reduce programmer effort and increase the ease with which a compiler can be ported to a new target machine, target code generators are sometimes generated automatically from a formal description of the machine. Automatically generated code generators all rely on some sort of pattern-matching algorithm to replace sequences of intermediate code instructions with equivalent sequences of target machine instructions. References to several such algorithms can be found in the Bibliographic Notes at the end of this chapter; details are beyond the scope of this book.
我们示例编译器结构的最后阶段包括寄存器分配和指令调度,这两者都可以视为特定于机器的代码改进。寄存器分配要求我们将早期阶段使用的无限虚拟寄存器映射到目标机器中可用的有界架构寄存器集上。如果没有足够的架构寄存器可供使用,我们可能需要生成额外的加载和存储,以在两个或多个虚拟寄存器之间多路复用给定的架构寄存器。指令调度(在 C-5.5 和 C-17.6 节中描述)包括重新排序每个基本块的指令,以尝试填充目标机器的管道。
The final phase of our example compiler structure consists of register allocation and instruction scheduling, both of which can be thought of as machine-specific code improvement. Register allocation requires that we map the unlimited virtual registers employed in earlier phases onto the bounded set of architectural registers available in the target machine. If there aren't enough architectural registers to go around, we may need to generate additional loads and stores to multiplex a given architectural register among two or more virtual registers. Instruction scheduling (described in Sections C-5.5 and C-17.6) consists of reordering the instructions of each basic block in an attempt to fill the pipeline(s) of the target machine.
在第 1.6 节中,我们将编译过程定义为相对于其余编译过程序列化的一个阶段或一系列阶段:它直到前面的阶段完成后才开始,并且在任何后续阶段开始之前完成。如果需要,可以将过程编写为单独的程序,从文件读取其输入并将其输出写入文件。两遍编译器特别常见。它们可以分为语义分析和中间代码生成,或中间代码生成和独立于机器的代码改进。在任一情况下,第一遍通常称为“前端”,第二遍称为“后端”。
In Section 1.6 we defined a pass of compilation as a phase or sequence of phases that is serialized with respect to the rest of compilation: it does not start until previous phases have completed, and it finishes before any subsequent phases start. If desired, a pass may be written as a separate program, reading its input from a file and writing its output to a file. Two-pass compilers are particularly common. They may be divided between semantic analysis and intermediate code generation or between intermediate code generation and machine-independent code improvement. In either case, the first pass is commonly referred to as the “front end” and the second pass as the “back end.”
与大多数编译器一样,我们的示例生成符号汇编语言作为其输出(一些编译器,包括 IBM 为 Power 系列编写的编译器,直接生成二进制机器代码)。汇编器(图 15.1中未显示)充当额外通道,为数据和代码片段分配地址,并将符号操作转换为其二进制编码。在大多数情况下,编译器的输入将由单个编译单元的源代码组成。汇编后,输出需要链接到应用程序的其他片段和各种预先存在的子例程库。某些链接工作可能会延迟到加载时(程序执行之前)或甚至到运行时(程序执行期间)。我们将在15.5到15.7节中讨论汇编和链接。
Like most compilers, our example generates symbolic assembly language as its output (a few compilers, including those written by IBM for the Power family, generate binary machine code directly). The assembler (notshown in Figure 15.1) behaves as an extra pass, assigning addresses to fragments of data and code, and translating symbolic operations into their binary encodings. In most cases, the input to the compiler will have consisted of source code for a single compilation unit. After assembly, the output will need to be linked to other fragments of the application, and to various preexisting subroutine libraries. Some of the work of linking may be delayed until load time (immediately prior to program execution) or even until run time (during program execution). We will discuss assembly and linking in Sections 15.5 through 15.7.
中间形式(IF)提供了与机器无关的代码改进阶段之间的连接,并在各个后端阶段继续表示程序。
An intermediate form (IF) provides the connection between phases of machine-independent code improvement, and continues to represent the program during the various back-end phases.
IF 可以根据其级别或机器依赖程度进行分类。高级 IF 通常基于树或有向无环图 (DAG),它们直接捕捉现代编程语言的层次结构。高级 IF 有助于某些类型的独立于机器的代码改进、增量程序更新(例如,在基于语言的编辑器中)和直接解释(大多数解释器都使用基于树的内部 IF)。由于树的允许结构可以通过一组产生式来正式描述(如第 4.6 节所述),因此可以将基于树的形式的操作写成属性语法。
IFs can be classified in terms of their level, or degree of machine dependence. High-level IFs are often based on trees or directed acyclic graphs (DAGs) that directly capture the hierarchical structure of modern programming languages. A high-level IF facilitates certain kinds of machine-independent code improvement, incremental program updates (e.g., in a language-based editor), and direct interpretation (most interpreters employ a tree-based internal IF). Because the permissible structure of a tree can be described formally by a set of productions (as described in Section 4.6), manipulations of tree-based forms can be written as attribute grammars.
最常见的中级 IF 采用三地址指令来处理简单的理想化机器,通常是具有无限数量的寄存器的机器。这些指令通常嵌入在控制流图中。由于典型的指令指定两个操作数、一个运算符和一个目标,因此三地址指令有时被称为四重指令。低级 IF 通常类似于某些特定目标机器的汇编语言,通常是目标代码将在其上执行的物理机器。
The most common medium-level IFs employ three-address instructions for a simple idealized machine, typically one with an unlimited number of registers. Often the instructions are embedded in a control flow graph. Since the typical instruction specifies two operands, an operator, and a destination, three-address instructions are sometimes called quadruples. Low-level IFs usually resemble the assembly language of some particular target machine, most often the physical machine on which the target code will execute.
具有针对多种不同目标架构的后端的编译器倾向于在高级或中级 IF 上完成尽可能多的工作,以便代码改进器中与机器无关的部分可以由不同的后端共享。相比之下,一些(但不是全部)为单一架构生成代码的编译器在相对低级的 IF 上执行大部分代码改进,这些 IF 紧密模仿目标机器的汇编语言。
Compilers that have back ends for several different target architectures tend to do as much work as possible on a high- or medium-level IF, so that the machine-independent parts of the code improver can be shared by different back ends. By contrast, some (but not all) compilers that generate code for a single architecture perform most code improvement on a comparatively low-level IF, closely modeled after the assembly language of the target machine.
在多语言编译器系列中,独立于源语言和目标机器的 IF 允许希望在m台机器上销售n种语言的编译器的软件供应商只构建n 个前端和m 个后端,而不是n × m 个集成编译器。即使在单语言编译器系列中,通用的、可能依赖于语言的 IF 也可以通过隔离需要更改的代码来简化移植到新机器的任务。在丰富的程序开发环境中,除了编译器的传递之外,可能还有各种工具可以理解和操作 IF。示例包括编辑器、汇编器、链接器、调试器、漂亮打印机和版本管理软件。在能够进行过程间(整个程序)代码改进的语言系统中,单独编译的模块和库可能只编译为 IF,而不是目标语言,将编译的最后阶段留给链接器。
In a multilanguage compiler family, an IF that is independent of both source language and target machine allows a software vendor who wishes to sell compilers for n languages on m machines to build just n front ends and m back ends, rather than n × m integrated compilers. Even in a single-language compiler family, a common, possibly language-dependent IF simplifies the task of porting to a new machine by isolating the code that needs to be changed. In a rich program development environment, there may be a variety of tools in addition to the passes of the compiler that understand and operate on the IF. Examples include editors, assemblers, linkers, debuggers, pretty-printers, and version-management software. In a language system capable of interprocedural (whole-program) code improvement, separately compiled modules and libraries may be compiled only to the IF, rather than the target language, leaving the final stages of compilation to the linker.
要存储在文件中,IF 需要线性表示。三地址指令序列自然是线性的。基于树的 IF 可以通过有序遍历进行线性化。控制流图等结构可以通过用相对于文件开头的索引替换指针来进行线性化。
To be stored in a file, an IF requires a linear representation. Sequences of three-address instructions are naturally linear. Tree-based IFs can be linearized via ordered traversal. Structures like control flow graphs can be linearized by replacing pointers with indices relative to the beginning of the file.
许多读者都熟悉 gcc 编译器。gcc 由自由软件基金会以开源形式发布,在学术界和工业界都得到广泛应用。标准发布版包括 C、C++、Objective-C、Ada、Fortran、Go 和 Java 的前端。其他语言的前端(包括 Cobol、Modula-2 和 3、Pascal 和 PL/I)可单独购买。C 编译器是最初的编译器,也是使用最广泛的编译器(gcc最初代表“GNU C 编译器”)。它有适用于数十种处理器架构的后端,包括所有具有商业意义的选项。还有不基于gcc的 GNU 实现,适用于大约二十多种其他语言。
Many readers will be familiar with the gcc compilers. Distributed as open source by the Free Software Foundation, gcc is used very widely in both academia and industry. The standard distribution includes front ends for C, C++, Objective-C, Ada, Fortran, Go, and Java. Front ends for additional languages, including Cobol, Modula-2 and 3, Pascal and PL/I, are separately available. The C compiler is the original, and the one most widely used (gcc originally stood for “GNU C compiler”). There are back ends for dozens of processor architectures, including all commercially significant options. There are also GNU implementations, not based on gcc, for some two dozen additional languages.
更深入地
IN MORE DEPTH
Gcc有三个主要的 IF。大多数(特定于语言的)前端在内部使用高级语法树形式的某种变体,称为 GENERIC。机器独立代码改进的早期阶段使用一种稍低级的树形式,称为 GIMPLE(仍然是高级 IF)。后期阶段使用一种线性形式,称为 RTL(寄存器传输语言)。RTL 是一种中级 IF,但比大多数 IF 的级别要高一点:它在一系列伪指令上覆盖了一个控制流图。多年来,RTL 一直是gcc的主要 IF。GIMPLE于 2005 年推出,作为一种更适合机器独立代码改进的形式。我们在配套网站上更详细地讨论了 GIMPLE 和 RTL。
Gcc has three main IFs. Most of the (language-specific) front ends employ, internally, some variant of a high-level syntax tree form known as GENERIC. Early phases of machine-independent code improvement use a somewhat lower-level tree form known as GIMPLE (still a high-level IF). Later phases use a linear form known as RTL (register transfer language). RTL is a medium-level IF, but a bit higher level than most: it overlays a control flow graph on of a sequence of pseudoinstructions. RTL was, for many years, the principal IF for gcc. GIMPLE was introduced in 2005 as a more suitable form for machine-independent code improvement. We consider GIMPLE and RTL in more detail on the companion site.
在简单和简洁至关重要的情况下,设计人员通常会求助于基于堆栈的语言。这种语言中的操作会从一个公共隐式堆栈中弹出参数并将结果推送到该堆栈中。没有命名操作数意味着基于堆栈的语言可以非常紧凑。在某些 HP 计算器中(练习 4.7),基于堆栈的表达式求值可最大限度地减少输入方程式所需的击键次数。对于嵌入式设备和打印机,Forth 和 Postscript 中基于堆栈的求值分别可减少内存和带宽要求(参见边栏 15.1)。
In situations where simplicity and brevity are paramount, designers often turn to stack-based languages. Operations in a such a language pop arguments from—and push results to—a common implicit stack. The lack of named operands means that a stack-based language can be very compact. In certain HP calculators (Exercise 4.7), stack-based expression evaluation serves to minimize the number of keystrokes required to enter equations. For embedded devices and printers, stack-based evaluation in Forth and Postscript serves to reduce memory and bandwidth requirements, respectively (see Sidebar 15.1).
当将代码从编译器传递到解释器或虚拟机时,基于堆栈的中级中间语言同样具有吸引力。四十年以前,P 代码(示例 1.15)使 Pascal 很容易移植到新机器上,并有助于加速该语言的采用。今天,Java 字节码的紧凑性有助于最大限度地缩短小程序的下载时间。通用中间语言 (CIL) 是 .NET 和公共语言基础结构 (CLI) 其他实现的 Java 字节码的类似物,同样紧凑且独立于机器。截至 2015 年,.NET 仅在 x86 和 ARM 上运行,但开源 Mono CLI 可用于所有主要指令集。我们将在第 16 章中详细讨论 Java 字节码和 CIL 。
Medium-level stack-based intermediate languages are similarly attractive when passing code from a compiler to an interpreter or virtual machine. Forty years ago, P-code (Example 1.15) made it easy to port Pascal to new machines, and helped to speed the language's adoption. Today, the compactness of Java bytecode helps minimize the download time for applets. Common Intermediate Language (CIL), the analogue of Java bytecode for .NET and other implementations of the Common Language Infrastructure (CLI), is similarly compact and machine independent. As of 2015, .NET runs only on the x86 and ARM, but the open-source Mono CLI is available for all the major instruction sets. We will consider Java bytecode and CIL in some detail in Chapter 16.
不幸的是,基于堆栈的 IF 并不适合许多代码改进技术:它限制了通过重新排序计算来消除冗余或提高管道性能的能力。因此,Java 字节码和 CIL 等语言往往主要用作外部格式,而不是编译器内代码的表示。
Unfortunately, stack-based IF is not well suited to many code improvement techniques: it limits the ability to eliminate redundancy or improve pipeline performance by reordering calculations. For this reason, languages like Java bytecode and CIL tend to be used mainly as an external format, not as a representation for code within a compiler.
与语义分析一样,中间代码生成可以用属性语法形式化,尽管它最常通过手写语法树的临时遍历来实现。为了清楚起见,我们在这里介绍一种属性语法。
Like semantic analysis, intermediate code generation can be formalized in terms of an attribute grammar, though it is most commonly implemented via handwritten ad hoc traversal of a syntax tree. We present an attribute grammar here for the sake of clarity.
在图 1.7中,我们展示了 GCD 程序的简单 x86 汇编语言。我们将使用属性语法示例在此处生成类似的版本,但适用于 RISC 类机器,并使用伪汇编符号。由于此符号现在旨在表示目标代码,而不是中级或低级中间代码,因此我们将假设一个固定的、有限的寄存器集,让人联想到真实机器。我们将保留几个寄存器(a1、a2、sp、rv )用于特殊目的;其他寄存器(r1..rk)将可用于临时值和表达式求值。
In Figure 1.7, we presented naive x86 assembly language for the GCD program. We will use our attribute grammar example to generate a similar version here, but for a RISC-like machine, and in pseudo-assembly notation. Because this notation is now meant to represent target code, rather than medium- or low-level intermediate code, we will assume a fixed, limited register set reminiscent of real machines. We will reserve several registers (a1, a2, sp, rv) for special purposes; others (r1 .. rk) will be available for temporary values and expression evaluation.
由于我们在示例中使用了符号表,并且符号表位于正式属性语法框架之外,因此我们必须在属性语法中添加一些额外的代码来进行存储管理。具体来说,在评估图 15.6的属性规则之前,我们必须遍历符号表,以便计算局部变量和参数(其中两个i和j出现在 GCD 程序中)的堆栈框架偏移量,并生成汇编程序指令来为全局变量(我们的程序没有)分配空间。存储分配和其他汇编程序指令将在15.5 节中详细讨论。
Because we use a symbol table in our example, and because symbol tables lie outside the formal attribute grammar framework, we must augment our attribute grammar with some extra code for storage management. Specifically, prior to evaluating the attribute rules of Figure 15.6, we must traverse the symbol table in order to calculate stack-frame offsets for local variables and parameters (two of which—i and j—occur in the GCD program) and in order to generate assembler directives to allocate space for global variables (of which our program has none). Storage allocation and other assembler directives will be discussed in more detail in Section 15.5.
需要强调的是,我们的寄存器分配算法虽然正确,但对机器资源的利用率很低。我们没有尝试重新组织表达式以尽量减少使用的寄存器数量,也没有尝试将常用变量长时间保存在寄存器中(避免加载和存储)。如果我们生成的是中级中间代码,而不是目标代码,我们将使用虚拟寄存器,而不是架构寄存器,并且每次需要时都会分配一个新的寄存器,永远不会重用一个寄存器来保存不同的值。虚拟寄存器到架构寄存器的映射将在编译过程的后期进行。
It should be emphasized that our register allocation algorithm, while correct, makes very poor use of machine resources. We have made no attempt to reorganize expressions to minimize the number of registers used, or to keep commonly used variables in registers over extended periods of time (avoiding loads and stores). If we were generating medium-level intermediate code, instead of target code, we would employ virtual registers, rather than architectural ones, and would allocate a new one every time we needed it, never reusing one to hold a different value. Mapping of virtual registers to architectural registers would occur much later in the compilation process.
汇编器、链接器和加载器通常对一对相关文件格式进行操作:可重定位目标代码和可执行目标代码。可重定位目标代码可作为链接器的输入;可以组合此格式的多个文件以创建可执行程序。可执行目标代码可作为加载器的输入:可将其载入内存并运行。可重定位目标文件包括以下描述性信息:
Assemblers, linkers, and loaders typically operate on a pair of related file formats: relocatable object code and executable object code. Relocatable object code is acceptable as input to a linker; multiple files in this format can be combined to create an executable program. Executable object code is acceptable as input to a loader: it can be brought into memory and run. A relocatable object file includes the following descriptive information:
导入表:标识指向地址未知的命名位置的指令,但推测位于尚未链接到此文件的其他文件中。
Import table: Identifies instructions that refer to named locations whose addresses are unknown, but are presumed to lie in other files yet to be linked to this one.
重定位表:标识引用当前文件内位置的指令,但必须在链接时进行修改以反映当前文件在最终可执行程序中的偏移量。
Relocation table: Identifies instructions that refer to locations within the current file, but that must be modified at link time to reflect the offset of the current file within the final, executable program.
导出表:列出当前文件中可能被其他文件引用的位置的名称和地址。
Export table: Lists the names and addresses of locations in the current file that maybe referred to in other files.
导入和导出的名称称为外部符号。
Imported and exported names are known as external symbols.
可执行目标文件的特点是它不包含对外部符号的引用(至少如果是静态链接的话——下面会详细介绍)。它还定义了执行的起始地址。可执行文件可能是或不是可重定位的,这取决于它是否包含上述表格。
An executable object file is distinguished by the fact that it contains no references to external symbols (at least if statically linked—more on this below). It also defines a starting address for execution. An executable file may or may not be relocatable, depending on whether it contains the tables above.
目标文件结构的细节因操作系统而异。但通常,目标文件分为几个部分,每个部分由链接器、加载器或操作系统以不同的方式处理。第一部分包括导入、导出和重定位表,以及对程序需要多少空间来存储未初始化的静态数据的指示。其他部分通常包括代码(指令)、只读数据(常量、case语句的跳转表等)、已初始化但可写的静态数据以及编译器保存的符号表和布局信息。初始描述部分由链接器和加载器使用。符号表部分由调试器和性能分析器使用(第16.3.2和16.3.3节)。这两个表通常都不会在运行时载入内存;大多数正在运行的程序都不需要它们(如果程序使用反射机制 [第 16.3.1 节] 来检查其自身的类型结构,则会出现例外情况)。
Details of object file structure vary from one operating system to another. Typically, however, an object file is divided into several sections, each of which is handled differently by the linker, loader, or operating system. The first section includes the import, export, and relocation tables, together with an indication of how much space will be required by the program for noninitialized static data. Other sections commonly include code (instructions), read-only data (constants, jump tables for case statements, etc.), initialized but writable static data, and symbol table and layout information saved by the compiler. The initial descriptive section is used by the linker and loader. The symbol table section is used by debuggers and performance profilers (Sections 16.3.2 and 16.3.3). Neither of these tables is usually brought into memory at run time; neither is needed by most running programs (an exception occurs in the case of programs that employ reflection mechanisms [Section 16.3.1] to examine their own type structure).
在可运行(加载)形式中,程序通常组织为若干段。在某些机器(例如 80286 或 PA-RISC)上,段对于汇编语言程序员来说是可见的,并且可以在指令中明确命名。在现代机器上,段更常见于操作系统以不同方式管理的地址空间子集。其中某些段(特别是代码、常量和初始化数据)对应于目标文件的各个部分。代码和常量通常是只读的,并且常组合在一个段中;如果程序试图修改它们,操作系统会安排接收中断。(作为对此类中断的响应,操作系统很可能会打印一条错误消息并终止程序。)初始化数据是可写的。在加载时,操作系统从磁盘读取代码、常量和初始化数据,或者安排在运行时读入它们,以响应“无效访问”(页面错误)中断或动态链接请求。
In its runnable (loaded) form, a program is typically organized into several segments. On some machines (e.g., the 80286 or PA-RISC), segments were visible to the assembly language programmer, and could be named explicitly in instructions. More commonly on modern machines, segments are simply subsets of the address space that the operating system manages in different ways. Some of them—code, constants, and initialized data in particular—correspond to sections of the object file. Code and constants are usually read-only, and are often combined in a single segment; the operating system arranges to receive an interrupt if the program attempts to modify them. (In response to such an interrupt it will most likely print an error message and terminate the program.) Initialized data are writable. At load time, the operating system either reads code, constants, and initialized data from disk, or arranges to read them in at run time, in response to “invalid access” (page fault) interrupts or dynamic linking requests.
除了代码、常量和初始化数据之外,典型的运行程序还有几个额外的段:
In addition to code, constants, and initialized data, the typical running program has several additional segments:
未初始化数据:可能在加载时分配,或根据需要分配以响应页面错误。通常用零填充,既可以为错误读取尚未写入的数据的程序提供可重复的症状,也可以通过阻止程序读取先前用户写入的页面内容来增强多用户系统的安全性。
Uninitialized data: May be allocated at load time or on demand in response to page faults. Usually zero-filled, both to provide repeatable symptoms for programs that erroneously read data they have not yet written, and to enhance security on multiuser systems, by preventing a program from reading the contents of pages written by previous users.
堆: 可以在加载时分配固定大小。更常见的是,初始大小较小,然后操作系统会自动扩展以响应超出当前段末尾的(故障)访问。
Stack: May be allocated in some fixed amount at load time. More commonly, is given a small initial size, and is then extended automatically by the operating system in response to (faulting) accesses beyond the current segment end.
堆:与堆栈类似,在加载时可能会分配一定数量的空间。更常见的是,堆的初始大小较小,然后根据堆管理库例程的明确请求(通过系统调用)进行扩展。
Heap: Like stack, may be allocated in some fixed amount at load time. More commonly, is given a small initial size, and is then extended in response to explicit requests (via system call) from heap-management library routines.
文件:在许多系统中,库例程允许程序将文件映射到内存中。映射例程与操作系统交互,为文件创建新段,并返回该段开头的地址。段的内容通常是根据需要从磁盘中提取的,以响应页面错误。
Files: In many systems, library routines allow a program to map a file into memory. The map routine interacts with the operating system to create a new segment for the file, and returns the address of the beginning of the segment. The contents of the segment are usually fetched from disk on demand, in response to page faults.
动态库:现代操作系统通常安排大多数程序共享常用库的单一代码副本(第 C-15.7 节)。从单个进程的角度来看,每个此类库往往占用一对段:一个用于共享代码,一个用于链接信息,一个用于库可能需要的任何可写数据的私有副本。
Dynamic libraries: Modern operating systems typically arrange for most programs to share a single copy of the code for popular libraries (Section C-15.7). From the point of view of an individual process, each such library tends to occupy a pair of segments: one for the shared code, one for linkage information and for a private copy of any writable data the library may need.
有些编译器会直接将源文件翻译成链接器可以接受的目标文件。更常见的是,它们会生成汇编语言,然后由汇编器处理该语言以创建目标文件。
Some compilers translate source files directly into object files acceptable to the linker. More commonly, they generate assembly language that must subsequently be processed by an assembler to create an object file.
在我们的示例中,我们始终使用符号(文本)表示法来表示代码。在编译器中,表示形式不是文本,但仍是符号,很可能由记录和链接列表组成。要将此符号表示转换为可执行代码,我们必须
In our examples we have consistently employed a symbolic (textual) notation for code. Within a compiler, the representation would not be textual, but it would still be symbolic, most likely consisting of records and linked lists. To translate this symbolic representation into executable code, we must
1. Replace opcodes and operands with their machine language encodings.
2. 用实际地址替换符号名称的使用。
2. Replace uses of symbolic names with actual addresses.
这些是装配工的主要任务。
These are the principal tasks of an assembler.
在计算机发展的早期,大多数程序员都使用汇编语言编写程序。为了简化汇编编程中繁琐而重复的部分,汇编程序通常提供大量宏扩展功能。随着现代高级语言的普及,这种以程序员为中心的特性已基本消失。如今,几乎所有的汇编语言程序都是由编译器编写的。
In the early days of computing, most programmers wrote in assembly language. To simplify the more tedious and repetitive aspects of assembly programming, assemblers often provided extensive macro expansion facilities. With the ubiquity of modern high-level languages, such programmer-centric features have largely disappeared. Almost all assembly language programs today are written by compilers.
实际上,SGI 汇编器实现了真实机器的“清理”版本。除了提供伪指令外,它还重新组织指令以隐藏延迟分支的存在(第 C-5.5.1 节)并提高处理器流水线的预期性能。此重组构成指令调度的最后一道关口(第 C-5.5.1 节和 C-17.6 节)。虽然这项工作可以由编译器处理,但整数除法示例等伪指令的存在强烈支持在汇编器中执行此操作。除了具有可能由相邻指令填充的两个分支延迟之外,扩展的除法序列还可以用作指令源来填充附近的分支、加载或功能单元延迟。
In effect, the SGI assembler implements a “cleaned-up” variant of the real machine. In addition to providing pseudoinstructions, it reorganizes instructions to hide the existence of delayed branches (Section C-5.5.1) and to improve the expected performance of the processor pipeline. This reorganization constitutes a final pass of instruction scheduling (Sections C-5.5.1 and C-17.6). Though the job could be handled by the compiler, the existence of pseudoinstructions like the integer division example argues strongly for doing it in the assembler. In addition to having two branch delays that might be filled by neighboring instructions, the expanded division sequence can be used as a source of instructions to fill nearby branch, load, or functional unit delays.
与编译器一样,汇编器通常分为几个阶段。如果输入是文本,则初始阶段将扫描和解析输入,并构建内部表示。在最常见的组织中,还有两个附加阶段。第一个阶段识别所有内部和外部(导入)符号,将位置分配给内部符号。这个阶段很复杂,因为某些指令的长度(在 CISC 机器上)或伪指令生成的实际指令的数量(在 RISC 机器上)可能取决于地址中的有效位数。给定符号值,最后阶段生成目标代码。
Like compilers, assemblers commonly work in several phases. If the input is textual, an initial phase scans and parses the input, and builds an internal representation. In the most common organization there are two additional phases. The first identifies all internal and external (imported) symbols, assigning locations to the internal ones. This phase is complicated by the fact that the length of some instructions (on a CISC machine) or the number of real instructions produced by a pseudoinstruction (on a RISC machine) may depend on the number of significant bits in an address. Given values for symbols, the final phase produces object code.
在目标文件中,.globl指令中提到的任何符号都必须出现在导出符号表中,并带有一个指示符号地址的条目。指令或指令中引用的任何符号,但未在输入程序中定义,都必须出现在导入符号表中,并带有一个条目,用于标识代码中出现此类引用的所有位置。最后,任何指令或数据(其值取决于当前文件在正在运行的程序的地址空间中的位置)都必须列在重定位表中。
Within the object file, any symbol mentioned in a .globl directive must appear in the table of exported symbols, with an entry that indicates the symbol's address. Any symbol referred to in a directive or an instruction, but not defined in the input program, must appear in the table of imported symbols, with an entry that identifies all places in the code at which such references occur. Finally, any instruction or datum whose value depends on the placement of the current file within the address space of a running program must be listed in the relocation table.
大多数语言实现(当然,所有用于构建大型程序的语言实现)都支持单独编译:程序的各个片段可以或多或少独立地进行编译和组装。编译后,这些片段(称为编译单元)由链接器“粘合在一起” 。在许多语言和环境中,程序员明确将程序划分为模块或文件,每个模块或文件都单独编译。集成程度更高的环境可能会放弃文件的概念,转而采用子例程数据库,每个子例程都单独编译。
Most language implementations—certainly all that are intended for the construction of large programs—support separate compilation: fragments of the program can be compiled and assembled more or less independently. After compilation, these fragments (known as compilation units) are “glued together” by a linker. In many languages and environments, the programmer explicitly divides the program into modules or files, each of which is separately compiled. More integrated environments may abandon the notion of a file in favor of a database of subroutines, each of which is separately compiled.
链接器的任务是将编译单元连接在一起。静态链接器在程序执行之前完成其工作,生成可执行目标文件。动态链接器(如第 C-15.7 节所述)在程序(第一部分)进入内存执行后完成其工作。
The task of a linker is to join together compilation units. A static linker does its work prior to program execution, producing an executable object file. A dynamic linker (described in Section C-15.7) does its work after the (first part of the) program has been brought into memory for execution.
每个要链接的编译单元都必须是可重定位的目标文件。通常,一些文件是通过编译正在构建的应用程序的片段生成的,而其他文件则是应用程序所需的预先存在的库包。由于大多数程序都使用库,因此即使是“单文件”应用程序通常也需要链接。
Each to-be-linked compilation unit must be a relocatable object file. Typically, some files will have been produced by compiling fragments of the application being constructed, while others will be preexisting library packages needed by the application. Since most programs make use of libraries, even a “one-file” application typically needs to be linked.
链接涉及两个子任务:重定位和外部引用的解析。有些作者将重定位称为加载,并将整个“连接在一起”的过程称为“链接加载”。其他作者(包括当前作者)使用“加载”来指将可执行目标文件放入内存中执行的过程。在非常简单的机器上,或者在操作系统非常简单的机器上,加载需要重定位。更常见的是,操作系统使用虚拟内存让每个程序都觉得它从某个标准地址启动。在许多系统中,加载还需要一定量的链接(第 C-15.7 节)。
Linking involves two subtasks: relocation and the resolution of external references. Some authors refer to relocation as loading, and call the entire “joining together” process “link-loading.” Other authors (including the current one) use “loading” to refer to the process of bringing an executable object file into memory for execution. On very simple machines, or on machines with very simple operating systems, loading entails relocation. More commonly, the operating system uses virtual memory to give every program the impression that it starts at some standard address. In many systems loading also entails a certain amount of linking (Section C-15.7).
库是一个挑战。许多库由数百个单独编译的程序片段组成,其中大多数程序片段对于任何特定的应用程序来说都不是必需的。应用程序。链接器不需要将整个库链接到每个应用程序中,而是需要搜索库以识别从主程序引用的片段。如果这些片段引用了其他片段,则还必须以递归方式包含这些片段。许多系统支持可重定位目标文件的特殊库格式。这种格式的库可以包含任意数量的代码和数据部分,以及将符号名称映射到它们出现的部分的索引。
Libraries present a bit of a challenge. Many consist of hundreds of separately compiled program fragments, most of which will not be needed by any particular application. Rather than link the entire library into every application, the linker needs to search the library to identify the fragments that are referenced from the main program. If these refer to additional fragments, then those must be included also, recursively. Many systems support a special library format for relocatable object files. A library in this format may contain an arbitrary number of code and data sections, together with an index that maps symbol names to the sections in which they appear.
在编译单元内,编译器强制执行静态语义规则。在单元之间,它使用模块头文件来强制执行与外部引用有关的规则。实际上,模块M的头文件就M与其用户的接口做出了一系列承诺。在编译M的主体时,编译器确保这些承诺得以兑现。但是,想象一下,如果我们编译了M的主体,然后在编译某个用户模块U之前更改了其头文件中某些子例程的参数数量和类型,会发生什么情况。如果两次编译都成功,那么M和U对如何解释在它们之间传递的参数的概念将非常不同;虽然它们可能仍然链接在一起,但在运行时可能会出现混乱。为了防止出现这种问题,我们必须确保每当M和U链接在一起时,它们都是使用相同版本的M头文件进行编译的。
Within a compilation unit, the compiler enforces static semantic rules. Across the boundaries between units, it uses module headers to enforce the rules pertaining to external references. In effect, the header for module M makes a set of promises regarding M's interface to its users. When compiling the body of M, the compiler ensures that those promises are kept. Imagine what could happen, however, if we compiled the body of M, and then changed the numbers and types of parameters for some of the subroutines in its header file before compiling some user module U. If both compilations succeed, then M and U will have very different notions of how to interpret the parameters passed between them; while they may still link together, chaos is likely to ensue at run time. To prevent this sort of problem, we must ensure whenever M and U are linked together that both were compiled using the same version of M's header.
在大多数基于模块的语言中,以下技术就足够了。在编译模块M的主体时,我们创建一个虚拟符号,其名称唯一地表征了M的标头的内容。在编译U的主体时,我们创建对虚拟符号的引用。只有当 M 和 U 在符号名称上一致时,将M和U链接在一起的尝试才会成功。
In most module-based languages, the following technique suffices. When compiling the body of module M we create a dummy symbol whose name uniquely characterizes the contents of M's header. When compiling the body of U we create a reference to the dummy symbol. An attempt to link M and U together will succeed only if they agree on the name of the symbol.
校验和策略确实要求我们知道何时使用模块头。不幸的是,如第 C-3.8 节所述,我们在 C 和 C++ 中不知道这一点:这些语言中的头文件只是一种编程约定,由语言预处理器的文本包含机制支持。大多数 C 实现并不在链接时强制接口的一致性;相反,程序员依靠配置管理工具(例如 Unix 的make)在必要时重新编译文件。此类工具通常由文件修改时间驱动。
The checksum strategy does require that we know when we're using a module header. Unfortunately, as described in Section C-3.8, we don't know this in C and C++: headers in these languages are simply a programming convention, supported by the textual inclusion mechanism of the language's preprocessor. Most implementations of C do not enforce consistency of interfaces at link time; instead, programmers rely on configuration management tools (e.g., Unix's make) to recompile files when necessary. Such tools are typically driven by file modification times.
C++ 的大多数实现采用了一种不同的方法,有时称为名称改编。目标文件中每个导入或导出符号的名称都是通过将程序源中的相应名称与其类型的表示连接起来而创建的。对于对象,类型由类名和其结构的简洁编码组成。对于函数,它由参数类型和返回值的编码组成。对于具有许多参数的复杂对象或函数,生成的名称可能会非常长。如果链接器将符号限制为某个过小的最大长度,则可以通过散列来压缩类型信息,但安全性会略有损失 [ SF88 ]。
Most implementations of C++ adopt a different approach, sometimes called name mangling. The name of each imported or exported symbol in an object file is created by concatenating the corresponding name from the program source with a representation of its type. For an object, the type consists of the class name and a terse encoding of its structure. For a function, it consists of an encoding of the types of the arguments and the return value. For complicated objects or functions of many arguments, the resulting names can be very long. If the linker limits symbols to some too-small maximum length, the type information can be compressed by hashing, at some small loss in security [SF88].
任何基于文件修改时间或校验和的技术都存在一个问题,即对头文件的微小更改(例如,修改注释或定义现有接口用户不需要的新常量)都可能导致文件无法正确链接。配置管理工具也存在类似的问题:微小的更改可能会导致工具不必要地重新编译文件。一些编程环境通过以小于编译单元的粒度跟踪更改来解决此问题 [ Tic86 ]。大多数只是需要重新编译。
One problem with any technique based on file modification times or checksums is that a trivial change to a header file (e.g., modification of a comment, or definition of a new constant not needed by existing users of the interface) can prevent files from linking correctly. A similar problem occurs with configuration management tools: a trivial change may cause the tool to recompile files unnecessarily. A few programming environments address this issue by tracking changes at a granularity smaller than the compilation unit [Tic86]. Most just live with the need to recompile.
在多用户系统中,一个程序(例如编辑器或 Web 浏览器)的多个实例同时执行是很常见的。为每个正在运行的程序实例分配内存空间用于单独的相同代码副本是非常浪费的。因此,许多操作系统会跟踪正在运行的程序,并设置内存映射表,以便同一程序的所有实例共享该程序代码段的同一份只读副本。每个实例都会收到其自己的可写数据段副本。代码段共享可以节省大量空间。但是,对于相似但不完全相同的程序实例,它不起作用。
On a multiuser system, it is common for several instances of a program (e.g., an editor or web browser) to be executing simultaneously. It would be highly wasteful to allocate space in memory for a separate, identical copy of the code of such a program for every running instance. Many operating systems therefore keep track of the programs that are running, and set up memory mapping tables so that all instances of the same program share the same read-only copy of the program's code segment. Each instance receives its own writable copy of the data segment. Code segment sharing can save enormous amounts of space. It does not work, however, for instances of programs that are similar but not identical.
许多程序集虽然不完全相同,但都具有大量共同的库代码——例如用于管理图形用户界面。如果每个应用程序如果有自己的库副本,则可能会浪费大量内存。此外,如果程序是静态链接的,则在单独的可执行目标文件中几乎相同的库副本上可能会浪费大量磁盘空间。
Many sets of programs, while not identical, have large amounts of library code in common—for example to manage a graphical user interface. If every application has its own copy of the library, then large amounts of memory may be wasted. Moreover, if programs are statically linked, then much larger amounts of disk space may be wasted on nearly identical copies of the library in separate executable object files.
更深入地
IN MORE DEPTH
在 20 世纪 90 年代初期,大多数操作系统供应商都采用了动态链接,以节省内存和磁盘空间。我们在配套网站上更详细地讨论了此选项。每个动态链接库都驻留在其自己的代码和数据段中。使用给定库的每个程序实例都拥有库数据段的私有副本,但共享库代码段的单个系统范围的只读副本。这些段可能在程序加载到内存时链接到代码的其余部分,或者在执行期间根据需要逐步链接。除了节省空间之外,动态链接还允许程序员或系统管理员安装库的向后兼容更新,而无需重建所有现有的可执行目标文件:下次运行时,每个程序都会自动获取库的新版本。
In the early 1990s, most operating system vendors adopted dynamic linking in order to save space in memory and on disk. We consider this option in more detail on the companion site. Each dynamically linked library resides in its own code and data segments. Every program instance that uses a given library has a private copy of the library's data segment, but shares a single system-wide read-only copy of the library's code segment. These segments maybe linked to the remainder of the code when the program is loaded into memory, or they may be linked incrementally on demand, during execution. In addition to saving space, dynamic linking allows a programmer or system administrator to install backward-compatible updates to a library without rebuilding all existing executable object files: the next time it runs, each program will obtain the new version of the library automatically.
在本章中,我们将注意力集中在编译器的后端,特别是代码生成、汇编和链接。
In this chapter we focused our attention on the back end ofthe compiler, and on code generation, assembly, and linking in particular.
编译器的中端和后端在内部结构上差异很大。我们讨论了一种可行的结构,其中语义分析之后按顺序进行中间代码生成、与机器无关的代码改进、目标代码生成和机器特定的代码改进(包括寄存器分配和指令调度)。语义分析器将语法树传递给中间代码生成器,中间代码生成器又将控制流图传递给与机器无关的代码改进器。在控制流图的节点内,我们建议用具有无限数量虚拟寄存器的伪汇编语言指令来表示代码。为了将代码改进的讨论推迟到第 17 章,我们还提出了一种更简单的后端结构,其中放弃代码改进,提前进行简单的寄存器分配,并将中间代码和目标代码生成合并为一个阶段。这个更简单的结构为我们讨论代码生成提供了背景。
Compiler middle and back ends vary greatly in internal structure. We discussed one plausible structure, in which semantic analysis is followed by, in order, intermediate code generation, machine-independent code improvement, target code generation, and machine-specific code improvement (including register allocation and instruction scheduling). The semantic analyzer passes a syntax tree to the intermediate code generator, which in turn passes a control flow graph to the machine-independent code improver. Within the nodes of the control flow graph, we suggested that code be represented by instructions in a pseudo-assembly language with an unlimited number of virtual registers. In order to delay discussion of code improvement to Chapter 17, we also presented a simpler back-end structure in which code improvement is dropped, naive register allocation happens early, and intermediate and target code generation are merged into a single phase. This simpler structure provided the context for our discussion of code generation.
我们还讨论了中间形式 (IF)。这些可以根据其级别或机器独立性程度进行分类。在配套站点上,我们考虑了 GIMPLE 和 RTL,它们是自由软件基金会 GNU 编译器的 IF。定义良好的 IF 有助于构建编译器系列,其中一种或多种语言的前端可以与许多机器的后端配对。在许多为虚拟机编译的系统中(将在第 16 章中详细讨论),编译器会生成基于堆栈的中级 IF。虽然这种 IF 通常不适合在编译器内部使用,但它可以很简单且非常紧凑。
We also discussed intermediate forms (IFs). These can be categorized in terms of their level, or degree of machine independence. On the companion site we considered GIMPLE and RTL, the IFs of the Free Software Foundation GNU compilers. A well-defined IF facilitates the construction of compiler families, in which front ends for one or more languages can be paired with back ends for many machines. In many systems that compile for a virtual machine (to be discussed at greater length in Chapter 16), the compiler produces a stack-based medium-level IF. While not generally suitable for use inside the compiler, such an IF can be simple and very compact.
中间代码生成通常通过语法树的临时遍历来执行。与语义分析一样,该过程可以用属性语法来形式化。我们展示了一个小示例语法的一部分,并用它来生成第1 章中介绍的 GCD 程序的代码。我们顺便指出,目标代码生成通常是全部或部分自动化的,使用代码生成器,该生成器将目标机器的形式化描述作为输入,并生成对指令序列或树执行模式匹配的代码。
Intermediate code generation is typically performed via ad hoc traversal of a syntax tree. Like semantic analysis, the process can be formalized in terms of attribute grammars. We presented part of a small example grammar and used it to generate code for the GCD program introduced in Chapter 1. We noted in passing that target code generation is often automated, in whole or in part, using a code generator generator that takes as input a formal description of the target machine and produces code that performs pattern matching on instruction sequences or trees.
在讨论汇编和链接时,我们描述了可重定位和可执行目标文件的格式,并讨论了名称解析和重定位的概念。我们注意到,虽然并非所有编译器都包含显式汇编阶段,但所有编译系统都必须能够生成用于调试目的的汇编代码,并且必须允许程序员用汇编程序编写专用例程。在使用汇编程序的编译器中,汇编阶段有时负责指令调度和其他低级代码改进。链接器则支持单独编译,方法是将多个编译生成的目标文件“粘合”在一起。在许多现代系统中,链接任务的很大一部分被延迟到加载时间甚至运行时,允许程序共享大型流行库的代码段。对于许多语言,链接器必须执行一定量的语义检查,以保证类型一致性。在更积极的优化编译系统中(本文未讨论),链接器还可以执行过程间代码改进。
In our discussion of assembly and linking we described the format of relocatable and executable object files, and discussed the notions of name resolution and relocation. We noted that while not all compilers include an explicit assembly phase, all compilation systems must make it possible to generate assembly code for debugging purposes, and must allow the programmer to write special-purpose routines in assembler. In compilers that use an assembler, the assembly phase is sometimes responsible for instruction scheduling and other low-level code improvement. The linker, for its part, supports separate compilation, by “gluing” together object files produced by multiple compilations. In many modern systems, significant portions of the linking task are delayed until load time or even run time, to allow programs to share the code segments of large, popular libraries. For many languages the linker must perform a certain amount of semantic checking, to guarantee type consistency. In more aggressive optimizing compilation systems (not discussed in this text), the linker may also perform interprocedural code improvement.
如第 1.5 节所述,典型的编程环境包含大量附加工具,包括调试器、性能分析器、配置和版本管理器、样式检查器、预处理器、漂亮打印机、测试系统以及细读和交叉引用实用程序。其中许多工具(特别是在集成良好的环境中)都由编译器直接支持。例如,许多工具都利用了目标文件中嵌入的符号表信息。性能分析器和测试系统通常依赖于编译器在子程序调用、循环边界和代码中的其他关键点插入的特殊检测代码。细读、样式检查和漂亮打印程序可以共享编译器的扫描器和解析器。配置工具通常依赖于文件间依赖关系列表(同样由编译器生成),以告知何时对大型系统的一部分的更改可能需要重新编译其他部分。
As noted in Section 1.5, the typical programming environment includes a host of additional tools, including debuggers, performance profilers, configuration and version managers, style checkers, preprocessors, pretty-printers, testing systems, and perusal and cross-referencing utilities. Many of these tools, particularly in well-integrated environments, are directly supported by the compiler. Many make use, for example, of symbol-table information embedded in object files. Performance profilers and testing systems often rely on special instrumentation code inserted by the compiler at subroutine calls, loop boundaries, and other key points in the code. Perusal, style-checking, and pretty-printing programs may share the compiler's scanner and parser. Configuration tools often rely on lists of interfile dependences, again generated by the compiler, to tell when a change to one part of a large system may require that other parts be recompiled.
15.1 如果你正在编写一个两遍编译器,为什么你会选择高级 IF 作为前端和后端之间的连接?为什么你会选择中级 IF?
15.1 If you were writing a two-pass compiler, why might you choose a high-level IF as the link between the front end and the back end? Why might you choose a medium-level IF?
15.2考虑一种类似 Ada 或 Modula-2 的语言,其中模块M可以分为规范(头文件)和实现(主体)文件,以便单独编译(第 10.2.1 节)。M的规范本身是否应该单独编译,还是应该由编译器在编译M的主体和使用M中定义的抽象的其他模块的主体的过程中直接读取它?如果规范被编译,输出应该由什么组成?
15.2 Consider a language like Ada or Modula-2, in which a module M can be divided into a specification (header) file and an implementation (body) file for the purpose of separate compilation (Section 10.2.1). Should M's specification itself be separately compiled, or should the compiler simply read it in the process of compiling M's body and the bodies of other modules that use abstractions defined in M? If the specification is compiled, what should the output consist of?
15.3 许多研究编译器(例如 SR [ AO93 ]、Cedar [ SZBH86 ]、Lynx [ Sco91 ] 和 Modula-3 [ Har92 ])都使用 C 作为其 IF。C 有详尽的文档,并且大部分与机器无关,而且 C 编译器比其他后端更广泛地可用。生成 C 有什么缺点?如何克服?
15.3 Many research compilers (e.g., for SR [AO93], Cedar [SZBH86], Lynx [Sco91], and Modula-3 [Har92]) have used C as their IF. C is well documented and mostly machine independent, and C compilers are much more widely available than alternative back ends. What are the disadvantages of generating C, and how might they be overcome?
15.4 尽可能多地列出即时编译器后端与更传统的编译器的不同之处。这些不同之处是由哪些设计目标决定的?
15.4 List as many ways as you can think of in which the back end of a just-in-time compiler might differ from that of a more conventional compiler. What design goals dictate the differences?
15.5 假设图 15.6中的k(临时寄存器的数量)为 4(对于现代机器来说,这是一个人为设定的小数字)。请给出一个表达式的例子,该表达式在我们的简单寄存器分配算法下会导致寄存器溢出。
15.5 Suppose that k (the number oftemporary registers) in Figure 15.6 is 4 (this is an artificially small number for modern machines). Give an example of an expression that will lead to register spilling under our naive register allocation algorithm.
15.6 修改图 15.6的属性文法,使得它将生成图 15.3的控制流图,而不是图 15.7的线性汇编代码。
15.6 Modify the attribute grammar of Figure 15.6 in such a way that it will generate the control flow graph of Figure 15.3 instead of the linear assembly code of Figure 15.7.
15.7向 图 15.6的文法中添加产生式和属性规则,以处理 Ada 风格的for循环(如第 6.5.1 节所述)。使用修改后的文法,将图 15.10的语法树手动翻译为伪汇编符号。将索引变量和上循环绑定在寄存器中。
15.7 Add productions and attribute rules to the grammar of Figure 15.6 to handle Ada-style for loops (described in Section 6.5.1). Using your modified grammar, hand-translate the syntax tree of Figure 15.10 into pseudo-assembly notation. Keep the index variable and the upper loop bound in registers.
15.8我们在 15.3 节中生成的代码存在一个问题(其中有很多问题),即它在运行时计算了本来可以在编译时计算的表达式的值。修改图 15.6中的语法以执行一种简单形式的常量折叠:只要运算符的两个操作数都是编译时常量,我们就应该在编译时计算该值,然后生成直接使用该值的代码。一定要考虑如何处理溢出。
15.8 One problem (of many) with the code we generated in Section 15.3 is that it computes at run time the value of expressions that could have been computed at compile time. Modify the grammar of Figure 15.6 to perform a simple form of constant folding: whenever both operands of an operator are compile-time constants, we should compute the value at compile time and then generate code that uses the value directly. Be sure to consider how to handle overflow.
15.9修改 图 15.6中的文法,以生成布尔表达式的跳转代码,如第 6.4.1 节所述。应假设短路求值(第 6.1.5 节)。
15.9 Modify the grammar of Figure 15.6 to generate jump code for Boolean expressions, as described in Section 6.4.1. You should assume short-circuit evaluation (Section 6.1.5).
15.10 我们的 GCD 程序没有使用子程序。扩展图 15.6的语法以处理没有参数的过程(可以自由地采用语法树结构上的任何合理约定)。确保为每个子程序生成适当的序言和结尾代码,并保存和恢复任何所需的临时寄存器。
15.10 Our GCD program did not employ subroutines. Extend the grammar of Figure 15.6 to handle procedures without parameters (feel free to adopt any reasonable conventions on the structure of the syntax tree). Be sure to generate appropriate prologue and epilogue code for each subroutine, and to save and restore any needed temporary registers.
15.11 图 15.6中的语法假设所有变量都是全局变量。在存在子程序的情况下,我们需要生成不同的代码(使用fp相对位移模式寻址)来访问局部变量和参数。在具有嵌套作用域的语言中,我们需要取消引用静态链(或索引显示)以访问既非局部也非全局的对象。假设我们正在编译一种具有嵌套子程序的语言,并使用静态链。修改图 15.6中的语法以生成代码来正确访问对象,而不管作用域如何。您可能会发现定义一个to_register子程序很有用,它可以生成加载给定对象的代码。一定要考虑l值和r值,以及值和结果传递的参数。
15.11 The grammar of Figure 15.6 assumes that all variables are global. In the presence of subroutines, we should need to generate different code (with fp-relative displacement mode addressing) to access local variables and parameters. In a language with nested scopes we should need to dereference the static chain (or index into the display) to access objects that are neither local nor global. Suppose that we are compiling a language with nested subroutines, and are using a static chain. Modify the grammar of Figure 15.6 to generate code to access objects correctly, regardless of scope. You may find it useful to define a to_register subroutine that generates the code to load a given object. Be sure to consider both l-values and r-values, and parameters passed by both value and result.
15.12–15.15 更深入。
15.12–15.15 In More Depth.
15.16 调查并描述您最常用的编译器的 IF。您能否指示编译器将其转储到您可以检查的文件中?除了编译器阶段之外,还有其他工具可以对 IF 进行操作(例如,调试器、代码改进器、配置管理器等)吗?其他语言或机器的编译器是否使用相同的 IF?
15.16 Investigate and describe the IF of the compiler you use most often. Can you instruct the compiler to dump it to a file which you can then inspect? Are there tools other than the compiler phases that operate on the IF (e.g., debuggers, code improvers, configuration managers, etc.)? Is the same IF used by compilers for other languages or machines?
15.17用你最喜欢的编程语言 实现图 15.6。定义适当的数据结构来表示语法树;然后通过临时树遍历为一些示例树生成代码。
15.17 Implement Figure 15.6 in your favorite programming language. Define appropriate data structures to represent a syntax tree; then generate code for some sample trees via ad hoc tree traversal.
15.18 扩展上一个练习的解决方案以处理各种其他语言特性。前面的练习中提到了几个有趣的选项。其他选项包括函数、一等子程序、case语句、记录、数组(特别是动态大小的数组)和迭代器。
15.18 Augment your solution to the previous exercise to handle various other language features. Several interesting options have been mentioned in earlier exercises. Others include functions, first-class subroutines, case statements, records, arrays (particularly those of dynamic size), and iterators.
15.19 找出您最喜欢的系统上有哪些工具可用于检查目标文件的内容(在 Unix 系统上,使用nm或objdump)。考虑某个由适量(比如说 3 到 6 个)编译单元组成的程序。使用适当的工具,列出每个编译单元中的导入和导出符号。然后将这些文件链接在一起。绘制一个地址图,显示各个代码段和数据段的放置位置。代码段中的哪些指令已因重定位而发生更改?
15.19 Find out what tools are available on your favorite system to inspect the content of object files (on a Unix system, use nm or objdump). Consider some program consisting of a modest number (three to six, say) of compilation units. Using the appropriate tool, list the imported and exported symbols in each compilation unit. Then link the files together. Draw an address map showing the locations at which the various code and data segments have been placed. Which instructions within the code segments have been changed by relocation?
15.20 在你最喜欢的 C++ 编译器中,调查外部符号名称中类型信息的编码。是否有奇怪的字符串在每个名称的末尾?如果可以,您能“逆向工程”生成它们的算法吗?要获取提示,请在您最喜欢的搜索引擎中输入“C++ 名称修改”。
15.20 In your favorite C++ compiler, investigate the encoding of type information in the names of external symbols. Are there strange strings of characters at the end of every name? If so, can you “reverse engineer” the algorithm used to generate them? For hints, type “C++ name mangling” into your favorite search engine.
15.21–15.25 更深入。
15.21–15.25 In More Depth.
标准编译器教科书(例如,Aho 等人 [ ALSU07 ]、Cooper 和 Torczon [ CT04 ]、Grune 等人 [ GBJ + 12 ]、Appel [ App97 ] 或 Fischer 等人 [ FCL10 ] 编写的教科书)是有关后端编译器技术的可访问信息来源。在 Muchnick [ Muc97 ] 的文本中可以找到更详细的信息。Fraser 和 Hanson 在他们的lcc编译器 [ FH95 ]中提供了有关代码生成和(简单)代码改进的大量详细信息。
Standard compiler textbooks (e.g., those by Aho et al. [ALSU07], Cooper and Torczon [CT04], Grune et al. [GBJ+12], Appel [App97], or Fischer et al. [FCL10]) are an accessible source of information on back-end compiler technology. More detailed information can be found in the text of Muchnick [Muc97]. Fraser and Hanson provide a wealth of detail on code generation and (simple) code improvement in their lcc compiler [FH95].
RTL 和 GIMPLE 的文档记录在 gcc Internals Manual 中,可从www.gnu.org/onlinedocs获取。Java 字节码的文档记录由 Lindholm 和 Yellin [ LYBB14 ] 记录。通用中间语言的文档记录由 Miller 和 Ragsdale [ MR04 ] 记录。
RTL and GIMPLE are documented in the gcc Internals Manual, available from www.gnu.org/onlinedocs. Java bytecode is documented by Lindholm and Yellin [LYBB14]. The Common Intermediate Language is described by Miller and Ragsdale [MR04].
Ganapathi、Fischer 和 Hennessy [ GFH82 ] 以及 Henry 和 Damron [ HD89 ] 提供了自动代码生成器的早期调查。当时使用最广泛的技术基于 LR 解析,由 Glanville 和 Graham [ GG78 ] 提出。Fraser 等人 [ FHP92 ] 描述了一种基于动态规划的更简单的方法。LLVM 目标独立代码生成器的文档可在 llvm.org/docs/CodeGenerator.html 找到。
Ganapathi, Fischer, and Hennessy [GFH82] and Henry and Damron [HD89] provide early surveys of automatic code generator generators. The most widely used technique from that era was based on LR parsing, and was due to Glanville and Graham [GG78]. Fraser et al. [FHP92] describe a simpler approach based on dynamic programming. Documentation for the LLVM Target-Independent Code Generator can be found at llvm.org/docs/CodeGenerator.html.
Beck [ Bec97 ] 为本世纪初的汇编程序、链接器和软件开发工具提供了很好的介绍。Gingell 等人描述了 SPARC 体系结构和 SunOS Unix 变体的共享库的实现 [ GLDW87 ]。Ho 和 Olsson 描述了一种特别雄心勃勃的 Unix 动态链接器 [ HO91 ]。Tichy 提出了一种编译系统,通过以比源文件更细的粒度跟踪依赖关系来避免不必要的重新编译 [ Tic86 ]。
Beck [Bec97] provides a good turn-of-the-century introduction to assemblers, linkers, and software development tools. Gingell et al. describe the implementation of shared libraries for the SPARC architecture and the SunOS variant of Unix [GLDW87]. Ho and Olsson describe a particularly ambitious dynamic linker for Unix [HO91]. Tichy presents a compilation system that avoids unnecessary recompilations by tracking dependences at a granularity finer than the source file [Tic86].
高级编程语言的每一个重要实现都会大量使用库。一些库例程非常简单:它们可能会将内存从一个地方复制到另一个地方,或者执行硬件不直接支持的算术函数。其他库例程则更为复杂。例如,堆管理例程会维护大量内部状态,缓冲或图形 I/O 的库也是如此。
Every nontrivial implementation of a high-level programming language makes extensive use of libraries. Some library routines are very simple: they may copy memory from one place to another, or perform arithmetic functions not directly supported by the hardware. Others are more sophisticated. Heap management routines, for example, maintain significant amounts of internal state, as do libraries for buffered or graphical I/O.
一般来说,我们使用术语运行时系统(或有时只使用运行时,不带连字符)来指代语言实现所依赖的一组库,这些库是正确运行所必需的。运行时的某些部分(比如堆管理)从子例程参数中获取所需的所有信息,并且可以轻松地用替代实现替换。然而,其他部分则需要对编译器或生成的程序有更广泛的了解。在简单的情况下,这些知识实际上只是编译器和运行时都遵守的一组约定(例如,子例程调用序列)。在更复杂的情况下,编译器会生成程序特定的元数据,运行时必须检查这些元数据才能完成其工作。例如,跟踪垃圾收集器(第 8.5.3 节)依赖于标识程序中所有“根指针”(所有全局、静态和基于堆栈的指针或引用变量)的元数据,以及每个引用和每个分配块的类型。
In general, we use the term run-time system (or sometimes just runtime, without the hyphen) to refer to the set of libraries on which the language implementation depends for correct operation. Some parts of the runtime, like heap management, obtain all the information they need from subroutine arguments, and can easily be replaced with alternative implementations. Others, however, require more extensive knowledge of the compiler or the generated program. In simpler cases, this knowledge is really just a set of conventions (e.g., for the subroutine calling sequence) that the compiler and runtime both respect. In more complex cases, the compiler generates program-specific metadata that the runtime must inspect to do its job. A tracing garbage collector (Section 8.5.3), for example, depends on metadata identifying all the “root pointers” in the program (all global, static, and stack-based pointer or reference variables), together with the type of every reference and of every allocated block.
前面的章节讨论过许多编译器/运行时集成的例子;我们将在侧栏 16.1 中回顾这些例子。列表的长度和复杂性通常意味着编译器和运行时系统必须一起开发。
Many examples of compiler/runtime integration have been discussed in previous chapters; we review these in Sidebar 16.1. The length and complexity of the list generally means that the compiler and the run-time system must be developed together.
有些语言(尤其是 C)的运行时系统非常小:执行给定源程序所需的大多数用户级代码要么由编译器直接生成,要么包含在独立于语言的库中。其他语言具有广泛的运行时系统。例如,C# 严重依赖于由通用语言基础结构 (CLI) 标准 [ Int12a ] 定义的运行时系统。
Some languages (notably C) have very small run-time systems: most of the user-level code required to execute a given source program is either generated directly by the compiler or contained in language-independent libraries. Other languages have extensive run-time systems. C#, for example, is heavily dependent on a run-time system defined by the Common Language Infrastructure (CLI) standard [Int12a].
虚拟机是使用编译器技术进行运行时管理和程序操作的一种日益流行的趋势的一部分。这一趋势是本章的主题。我们将在第 16.1 节中更详细地讨论虚拟机。为了避免模拟非本机指令集的开销,许多虚拟机使用即时(JIT) 编译器将其指令集转换为底层硬件的指令集。有些甚至可能在程序运行后调用编译器来编译新发现的组件或根据程序、其输入或底层系统的动态发现的属性优化代码。使用相关技术,一些语言实现执行二进制翻译以将为某台机器编译的程序重新定位到另一台机器上运行,或执行二进制重写以检测或优化已经为当前机器编译的程序。我们将在第16.2 节中讨论这些各种形式的机器代码后期绑定。最后,在第 16.3 节中,我们将讨论运行时机制来检查或修改正在运行的程序的状态。符号调试器以及分析和性能分析工具都需要这样的机制。它们还可能支持反射,这允许程序在运行时检查和推理其自身的状态。
Virtual machines are part of a growing trend toward run-time management and manipulation of programs using compiler technology. This trend is the subject of this chapter. We consider virtual machines in more detail in Section 16.1. To avoid the overhead of emulating a non-native instruction set, many virtual machines use a just-in-time (JIT) compiler to translate their instruction set to that of the underlying hardware. Some may even invoke the compiler after the program is running, to compile newly discovered components or to optimize code based on dynamically discovered properties of the program, its input, or the underlying system. Using related technology, some language implementations perform binary translation to retarget programs compiled for one machine to run on another machine, or binary rewriting to instrument or optimize programs that have already been compiled for the current machine. We consider these various forms of late binding of machine code in Section 16.2. Finally, in Section 16.3, we consider run-time mechanisms to inspect or modify the state of a running program. Such mechanisms are needed by symbolic debuggers and by profiling and performance analysis tools. They may also support reflection, which allows a program to inspect and reason about its own state at run time.
虚拟机( VM) 提供完整的编程环境:其应用程序编程接口 (API) 包括正确执行在其上运行的程序所需的一切。我们通常将术语“VM”保留用于抽象级别与硬件实现的计算机相当的环境。(例如,Smalltalk 或 Python 解释器通常不被描述为虚拟机,因为它的抽象级别太高,但这是一种主观称呼。)
A virtual machine (VM) provides a complete programming environment: its application programming interface (API) includes everything required for correct execution of the programs that run above it. We typically reserve use of the term “VM” to environments whose level of abstraction is comparable to that of a computer implemented in hardware. (A Smalltalk or Python interpreter, for example, is usually not described as a virtual machine, because its level of abstraction is too high, but this is a subjective call.)
每个虚拟机 API 都包含一个指令集架构 (ISA),用于表达程序。这可能与某些现有的物理机,或者它可能是一个人工指令集,旨在更易于在软件中实现并使用编译器生成。VM API 的其他部分可能支持 I/O、调度或由库或物理机的操作系统 (OS) 提供的其他服务。
Every virtual machine API includes an instruction set architecture (ISA) in which to express programs. This may be the same as the instruction set of some existing physical machine, or it may be an artificial instruction set designed to be easier to implement in software and to generate with a compiler. Other portions of the VM API may support I/O, scheduling, or other services provided by a library or by the operating system (OS) of a physical machine.
实际上,虚拟机通常被描述为系统虚拟机或进程虚拟机。系统虚拟机忠实地模拟运行标准操作系统所需的所有硬件设施,包括特权和非特权指令、内存映射 I/O、虚拟内存和中断设施。相比之下,进程虚拟机提供了单个用户级进程所需的环境:指令集的非特权子集以及 I/O 和其他服务的库级接口。
In practice, virtual machines tend to be characterized as either system VMs or process VMs. A system VM faithfully emulates all the hardware facilities needed to run a standard OS, including both privileged and unprivileged instructions, memory-mapped I/O, virtual memory, and interrupt facilities. By contrast, a process VM provides the environment needed by a single user-level process: the unprivileged subset of the instruction set and a library-level interface to I/O and other services.
系统 VM 通常由虚拟机监视器(VMM) 或虚拟机管理程序管理,它将单个物理机器多路复用到一组“客户”操作系统中,每个操作系统都在自己的虚拟机中运行。第一个广泛使用的 VMM 是 IBM 的 CP/CMS,它于 1967 年首次亮相。IBM 并没有构建一个能够支持多个用户的操作系统,而是使用 CP(“控制程序”)VMM 来创建一组虚拟机,每个虚拟机都运行一个轻量级的单用户操作系统 (CMS)。近年来,VMM 在云计算的兴起中发挥了核心作用,它允许托管中心在大量(相互隔离的)客户操作系统之间共享物理机器。如果客户工作负载在裸机上运行,中心可以更轻松地监控和管理其工作负载 — — 它甚至可以将正在运行的操作系统从一台机器迁移到另一台机器,以平衡客户之间的负载或清理机器以进行硬件维护。系统虚拟机在个人电脑上也越来越受欢迎,其中 VMware Fusion 和 Parallels Desktop 等产品允许用户同时在多个操作系统上运行程序。
System VMs are often managed by a virtual machine monitor (VMM) or hypervisor, which multiplexes a single physical machine among a collection of “guest” operating systems, each of which runs in its own virtual machine. The first widely available VMM was IBM's CP/CMS, which debuted in 1967. Rather than build an operating system capable of supporting multiple users, IBM used the CP (“control program”) VMM to create a collection of virtual machines, each of which ran a lightweight, single-user operating system (CMS). In recent years, VMMs have played a central role in the rise of cloud computing, by allowing a hosting center to share physical machines among a large number of (mutually isolated) guest OSes. The center can monitor and manage its workload more easily if customer workloads were running on bare hardware—it can even migrate running OSes from one machine to another, to balance load among customers or to clear machines for hardware maintenance. System VMs are also increasingly popular on personal computers, where products like VMware Fusion and Parallels Desktop allow users to run programs on top of more than one OS at once.
然而,对编程语言设计和实现影响最大的是进程虚拟机。与系统虚拟机一样,该技术已有几十年的历史:例如,示例 1.15中描述的 P 代码虚拟机可以追溯到 20 世纪 70 年代初。进程虚拟机最初被认为是一种提高程序可移植性和在新硬件上快速“引导”语言的方法。传统的缺点是由于对抽象指令集的解释而导致的性能不佳。可移植性和性能之间的权衡一直持续到 20 世纪 90 年代后期,当时 Java 的早期版本通常比 Fortran 或 C 等传统编译语言慢一个数量级。然而,随着即时编译的引入,Java 虚拟机 (JVM) 和公共语言基础结构 (CLI) 的现代实现已经可以与传统语言在本机硬件上的性能相媲美。我们将在第16.1.1和16.1.2节中讨论这些系统。
It is process VMs, however, that have had the greatest impact on programming language design and implementation. As with system VMs, the technology is decades old: the P-code VM described in Example 1.15, for example, dates from the early 1970s. Process VMs were originally conceived as a way to increase program portability and to quickly “bootstrap” languages on new hardware. The traditional downside was poor performance due to interpretation of the abstract instruction set. The tradeoff between portability and performance remained valid through the late 1990s, when early versions of Java were typically an order of magnitude slower than traditionally compiled languages like Fortran or C. With the introduction of just-in-time compilation, however, modern implementations of the Java Virtual Machine (JVM) and the Common Language Infrastructure (CLI) have come to rival the performance of traditional languages on native hardware. We will consider these systems in Sections 16.1.1 and 16.1.2.
JVM 和 CLI 都使用基于堆栈的中间形式 (IF):分别是 Java 字节码和 CLI 通用中间语言 (CIL)。如第 15.2.2 节所述,缺少命名操作数意味着基于堆栈的 IF 可以非常紧凑 - 这对于通过互联网分发的代码(例如小程序)尤为重要。同时,需要按堆栈顺序计算所有内容意味着中间结果通常不能保存在寄存器中并重复使用。在许多情况下,与基于寄存器的机器的相应代码相比,基于堆栈的表达式代码将占用更少的字节,但指定更多的指令。
Both the JVM and the CLI use a stack-based intermediate form (IF): Java byte-code and CLI Common Intermediate Language (CIL), respectively. As described in Section 15.2.2, the lack of named operands means that stack-based IF can be very compact—a feature of particular importance for code (e.g., applets) distributed over the Internet. At the same time, the need to compute everything in stack order means that intermediate results cannot generally be saved in registers and reused. In many cases, stack-based code for an expression will occupy fewer bytes, but specify more instructions, than corresponding code for a register-based machine.
最终成为 Java 的语言的开发始于 1990-1991 年,当时 Sun Microsystems 的 Patrick Naughton、James Gosling 和 Mike Sheridan 开始研究嵌入式设备的编程系统。该系统的早期版本于 1992 年启动并运行,当时该语言被称为 Oak。1994 年,在尝试打入有线电视机顶盒市场失败后,该项目重新定位到 Web 浏览器,并更名为 Java。
Development of the language that eventually became Java began in 1990–1991, when Patrick Naughton, James Gosling, and Mike Sheridan of Sun Microsystems began work on a programming system for embedded devices. An early version of this system was up and running in 1992, at which time the language was known as Oak. In 1994, after unsuccessful attempts to break into the market for cable TV set-top boxes, the project was retargeted to web browsers, and the name was changed to Java.
Java 的第一个公开版本发布于 1995 年。当时,JVM 中的代码完全是解释型的。1998 年,随着 Java 2 的发布,添加了 JIT 编译器。尽管 Java 未被任何常见机构(ANSI、ISO、ECMA)标准化,但它的定义足够完善,可以接受各种编译器和 JVM。Oracle 的javac编译器和 HotSpot JVM 于 2006 年作为开源发布,是迄今为止使用最广泛的。Jikes RVM(研究虚拟机)是一种自托管 JVM,用 Java 本身编写,广泛用于 VM 研究。几家公司都有自己的专有 JVM 和类库,旨在在特定机器或特定市场上提供竞争优势。
The first public release of Java occurred in 1995. At that time code in the JVM was entirely interpreted. A JIT compiler was added in 1998, with the release of Java 2. Though not standardized by any of the usual agencies (ANSI, ISO, ECMA), Java is sufficiently well defined to admit a wide variety of compilers and JVMs. Oracle's javac compiler and HotSpot JVM, released as open source in 2006, are by far the most widely used. The Jikes RVM (Research Virtual Machine) is a self-hosting JVM, written in Java itself, and widely used for VM research. Several companies have their own proprietary JVMs and class libraries, designed to provide a competitive edge on particular machines or in particular markets.
JVM 提供的接口旨在成为 Java 编译器的一个有吸引力的目标。它为所有(且仅)内置和Java 语言定义的引用类型。它还强制执行明确赋值(第 6.1.3 节)和类型安全。最后,它内置了对许多 Java 语言功能和标准库包的支持,包括异常、线程、垃圾收集、反射、动态加载和安全性。
The interface provided by the JVM was designed to be an attractive target for a Java compiler. It provides direct support for all (and only) the built-in and reference types defined by the Java language. It also enforces both definite assignment (Section 6.1.3) and type safety. Finally, it includes built-in support for many of Java's language features and standard library packages, including exceptions, threads, garbage collection, reflection, dynamic loading, and security.
当然,Java 字节码并不要求必须从 Java 源代码生成。针对 JVM 的编译器适用于许多其他语言,包括 Ruby、JavaScript、Python 和 Scheme(传统上它们都是解释型的),以及 C、Ada、Cobol 和其他传统上编译型的语言。3甚至还有汇编程序允许程序员直接编写 Java 字节码。对于编译器和汇编程序来说,主要要求是它们生成正确的类文件。这些文件具有 JVM 可以理解的特殊格式,并且必须满足各种结构和语义约束。
Of course, nothing requires that Java bytecode be produced from Java source. Compilers targeting the JVM exist for many other languages, including Ruby, JavaScript, Python, and Scheme (all ofwhich are traditionally interpreted), as well as C, Ada, Cobol, and others, which are traditionally compiled.3 There are even assemblers that allow programmers to write Java bytecode directly. The principal requirement, for both compilers and assemblers, is that they generate correct class files. These have a special format understood by the JVM, and must satisfy a variety of structural and semantic constraints.
在启动时,JVM 通常会获得包含静态方法main的类文件的名称。它将此类加载到内存中,验证它是否满足各种必需的约束,分配所有静态字段,将其链接到任何预加载的库例程,并调用程序员为类或静态字段提供的任何初始化代码。最后,它在单个线程中调用main 。其他类(初始类所需的)可以根据需要立即或延迟加载。可以通过调用Thread 类的(内置)方法创建其他线程。以下三个小节提供了有关 JVM 存储管理、类文件格式和 Java 字节码指令集的更多详细信息。
At start-up time, a JVM is typically given the name of a class file containing the static method main. It loads this class into memory, verifies that it satisfies a variety of required constraints, allocates any static fields, links it to any preloaded library routines, and invokes any initialization code provided by the programmer for classes or static fields. Finally, it calls main in a single thread. Additional classes (needed by the initial class) may be loaded either immediately or lazily on demand. Additional threads maybe created via calls to the (built-in) methods of class Thread. The three following subsections provide additional details on JVM storage management, the format of class files, and the Java bytecode instruction set.
JVM 中的存储分配机制与 Java 语言的机制相似。JVM 有一个全局常量池、一组寄存器和一个用于每个线程的堆栈、一个用于保存可执行字节码的方法区和一个用于动态分配对象的堆。
Storage allocation mechanisms in the JVM mirror those of the Java language. There is a global constant pool, a set of registers and a stack for each thread, a method area to hold executable bytecode, and a heap for dynamically allocated objects.
在 JVM 上运行的程序以单个线程开始。通过分配和初始化内置类Thread的新对象,然后调用其start方法来创建其他线程。每个线程都有一小组基址寄存器、一个方法调用框架堆栈和一个可选的传统堆栈,用于调用本机(非 Java)方法。
A program running on the JVM begins with a single thread. Additional threads are created by allocating and initializing a new object of the build-in class Thread, and then calling its start method. Each thread has a small set of base registers, a stack of method call frames, and an optional traditional stack on which to call native (non-Java) methods.
方法调用堆栈上的每个框架都包含一个局部变量数组、一个用于评估方法表达式的操作数堆栈和一个指向常量池的引用,该引用标识被调用方法的动态链接所需的信息。局部变量中包含了形式参数的空间。不同时处于活动状态的变量可以共享数组中的一个槽位;这意味着同一个槽位可以在不同时间用于不同类型的数据。
Each frame on the method call stack contains an array of local variables, an operand stack for evaluation of the method's expressions, and a reference into the constant pool that identifies information needed for dynamic linking of called methods. Space for formal parameters is included among the local variables. Variables that are not live at the same time can share a slot in the array; this means that the same slot may be used at different times for data of different types.
由于 Java 字节码是面向堆栈的,因此算术和逻辑指令的操作数和结果保存在当前方法框架的操作数堆栈中,而不是寄存器中。隐式地,JVM 指令集要求每个线程有四个寄存器,用于保存程序计数器和对当前框架的引用、操作数堆栈的顶部以及局部变量数组的底部。
Because Java bytecode is stack oriented, operands and results of arithmetic and logic instructions are kept in the operand stack of the current method frame, rather than in registers. Implicitly, the JVM instruction set requires four registers per thread, to hold the program counter and references to the current frame, the top of the operand stack, and the base of the local variable array.
局部变量数组和操作数堆栈中的槽位始终为 32 位宽。较小类型的数据会被填充;长整型和双精度型数据各占用两个槽位。操作数堆栈所需的最大深度可由编译器静态确定,从而便于在框架中预分配空间。
Slots in the local variable array and the operand stack are always 32 bits wide. Data of smaller types are padded; long and double data take two slots each. The maximum depth required for the operand stack can be determined statically by the compiler, making it easy to preallocate space in the frame.
为了与 Java 语言的类型系统保持一致,局部变量数组或操作数堆栈中的数据始终是引用或内置标量类型的值。结构化数据(对象和数组)必须始终位于堆。它们是使用new和newarray指令动态分配的。它们通过垃圾收集自动回收。收集算法的选择由 JVM 的实现者决定。
In keeping with the type system of the Java language, a datum in the local variable array or the operand stack is always either a reference or a value of a built-in scalar type. Structured data (objects and arrays) must always lie in the heap. They are allocated, dynamically, using the new and newarray instructions. They are reclaimed automatically via garbage collection. The choice of collection algorithm is left to the implementor of the JVM.
为了便于线程间共享,Java 语言提供了与监视器等同的功能,每个对象都有一个锁和一个隐式条件变量,如第 13.4.3 节所述。JVM 为这种同步方式提供直接支持。堆中的每个对象都有一个关联的互斥锁;在典型的实现中,锁维护一组等待进入监视器的线程。此外,每个对象都有一组关联的线程,这些线程正在等待监视器的条件变量。4使用monitorenter指令获取锁,使用monitorexit指令释放锁。大多数JVM 坚持这些调用以匹配的嵌套对的形式出现,并且给定方法中获取的每个锁都应在同一方法中释放(任何正确的 Java 语言编译器都会遵循这些规则)。
To facilitate sharing among threads, the Java language provides the equivalent of monitors with a lock and a single, implicit condition variable per object, as described in Section 13.4.3. The JVM provides direct support for this style of synchronization. Each object in the heap has an associated mutual exclusion lock; in a typical implementation, the lock maintains a set of threads waiting for entry to the monitor. In addition, each object has an associated set of threads that are waiting for the monitor's condition variable.4 Locks are acquired with the monitorenter instruction and released with the monitorexit instruction. Most JVMs insist that these calls appear in matching nested pairs, and that every lock acquired within a given method be released within the same method (any correct compiler for the Java language will follow these rules).
对共享对象的访问一致性由 Java 内存模型控制,我们在第 13.3.3 节中简要介绍了该模型。非正式地说,每个线程的行为都好像它保留了堆的私有缓存。当线程释放监视器或写入易失性变量时,JVM 必须确保对该线程缓存的所有先前更新都已写回内存。当线程进入监视器或读取易失性变量时,JVM 必须(实际上)清除线程的缓存,以便后续读取会导致从内存中重新加载位置。当然,实际实现不会执行显式写回或失效;它们从硬件的缓存一致性协议提供的内存模型开始,并在需要时使用内存屏障(隔离)指令来避免不可接受的排序。
Consistency of access to shared objects is governed by the Java memory model, which we considered briefly in Section 13.3.3. Informally, each thread behaves as if it kept a private cache of the heap. When a thread releases a monitor or writes a volatile variable, the JVM must ensure that all previous updates to the thread's cache have been written back to memory. When a thread enters a monitor or reads a volatile variable, the JVM must (in effect) clear the thread's cache so that subsequent reads cause locations to be reloaded from memory. Of course, actual implementations don't perform explicit write-backs or invalidations; they start with the memory model provided by the hardware's cache coherence protocol and use memory barrier (fence) instructions where needed to avoid unacceptable orderings.
从物理上讲,JVM 类文件以字节流的形式存储。通常,这些字节会占用操作系统提供的一些实际文件,但它们也可以很容易地成为数据库中的记录。在许多系统中,多个类文件可能组合成一个 Java 存档 ( .jar ) 文件。
Physically, a JVM class file is stored as a stream of bytes. Typically these occupy some real file provided by the operating system, but they could just as easily be a record in a database. On many systems, multiple class files maybe combined into a Java archive (.jar) file.
从逻辑上讲,类文件具有明确定义的层次结构。它以“魔法数字”(0x_cafe_babe)开头,如边栏 14.4 中所述。接下来是
Logically, a class file has a well-defined hierarchical structure. It begins with a “magic number” (0x_cafe_babe), as described in Sidebar 14.4. This is followed by
■ Major and minor version numbers of the JVM for which the file was created
■ 常量池
■ The constant pool
■ 当前类及其超类的常量池索引
■ Indices into the constant pool for the current class and its superclass
■ 描述类的超接口、字段和方法的表格
■ Tables describing the class's superinterfaces, fields, and methods
由于 JVM 比真实机器更简洁、更抽象,因此 Java 类文件结构比典型的目标文件(第 15.4 节)更简洁、更抽象。明显缺少的是处理典型真实机器上将地址嵌入指令的多种方式所需的大量重定位信息。取而代之的是,类文件中的字节码指令包含对常量池中符号名称的引用。当代码动态链接时,这些符号名称将成为方法区域中的引用。(或者,当代码进行 JIT 编译时,它们可能会成为经过适当编码的真实机器地址。)同时,类文件包含可执行目标文件中通常没有的大量信息。示例包括类、字段和方法的访问标志(public、private、protected、static、final、synchronized、native、abstract、strictfp);内置于文件结构中的符号表信息(而不是可选附加组件);以及针对诸如抛出异常或进入或离开监视器等高级概念的特殊指令。
Because the JVM is both cleaner and more abstract than a real machine, the Java class file structure is both cleaner and more abstract than a typical object file (Section 15.4). Conspicuously missing is the extensive relocation information required to cope with the many ways that addresses are embedded into instructions on a typical real machine. In place of this, bytecode instructions in a class file contain references to symbolic names in the constant pool. These become references into the method area when code is dynamically linked. (Alternatively, they may become real machine addresses, appropriately encoded, when the code is JIT compiled.) At the same time, class files contain extensive information not typically found in an executable object file. Examples include access flags for classes, fields, and methods (public, private, protected, static, final, synchronized, native, abstract, strictfp); symbol table information that is built into the structure of the file (rather than an optional add-on); and special instructions for such high-level notions as throwing an exception or entering or leaving a monitor.
方法(或构造函数或类初始化程序)的字节码出现在类文件的方法表的条目中。它附带以下内容:
The bytecode for a method (or for a constructor or a class initializer) appears in an entry in the class file's method table. It is accompanied by the following:
■ An indication of the number of local variables, including parameters
■ 操作数堆栈所需的最大深度
■ The maximum depth required in the operand stack
■ 异常处理程序信息表,其中每一项指示
■ A table of exception handler information, each entry ofwhich indicates
– The bytecode range covered by this handler
处理 程序本身的地址(代码中的索引)
– The address (index in the code) of the handler itself
– 捕获的异常类型(常量池的索引)
– The type of exception caught (an index into the constant pool)
■ 调试器的可选信息:具体来说,将字节码地址映射到原始源代码中的行号的表和/或指示哪个源代码变量在字节码的哪个位置占用哪个 JVM 局部变量的表。
■ Optional information for debuggers: specifically, a table mapping bytecode addresses to line numbers in the original source code and/or a table indicating which source code variable(s) occupy which JVM local variables at which points in the bytecode.
Java 字节码设计得既简单又紧凑。正交性是次要的考虑因素。每条指令都以单字节操作码开头。参数(如果有)占据后续字节,其值按大端顺序给出。除了用于switch语句的两个例外,为了紧凑起见,参数是不对齐的。但大多数指令实际上不需要参数。典型的硬件对命名寄存器中的值执行算术运算,而字节码从当前方法帧的操作数堆栈中弹出参数并将结果推送到该堆栈。此外,即使是加载和存储也经常使用单个字节。例如,对于局部变量数组中的前四个条目,每个条目都有特殊的单字节整数存储指令。同样,也有特殊指令将值 -1、0、1、2、3、4 和 5 推送到操作数堆栈。
Java bytecode was designed to be both simple and compact. Orthogonality was a strictly secondary concern. Every instruction begins with a single-byte opcode. Arguments, if any, occupy subsequent bytes, with values given in big-endian order. With two exceptions, used for switch statements, arguments are unaligned, for compactness. Most instructions, however, actually don't need an argument. Where typical hardware performs arithmetic on values in named registers, bytecode pops arguments from, and pushes result to, the operand stack of the current method frame. Moreover, even loads and stores can often use a single byte. There are, for example, special one-byte integer store instructions for each of the first four entries in the local variable array. Similarly, there are special instructions to push the values −1, 0, 1, 2, 3, 4, and 5 onto the operand stack.
从 Java 8 开始,JVM 定义了 256 个可能的操作码值中的 205 个。其中五个用于特殊用途(未使用、nop、调试器断点、实现相关)。其余可分为以下类别:
As of Java 8, the JVM defines 205 of the 256 possible opcode values. Five of these serve special purposes (unused, nop, debugger breakpoints, implementation dependent). The remainder can be organized into the following categories:
Load/store: move values back and forth between the operand stack and the local variable array.
算术:对操作数堆栈中的值执行整数或浮点运算。
Arithmetic: perform integer or floating point operations on values in the operand stack.
类型转换:内置类型(byte、char、short、int、long、float 和 double)之间的“加宽”或“缩小”值。缩小可能会导致精度损失,但绝不会引发异常。
Type conversion: “widen” or “narrow” values among the built-in types (byte, char, short, int, long, float, and double). Narrowing may result in a loss of precision but never an exception.
对象管理:创建或查询对象和数组的属性;访问字段和数组元素。
Object management: create or query the properties of objects and arrays; access fields and array elements.
操作数堆栈管理:压入和弹出;复制;交换。
Operand stack management: push and pop; duplicate; swap.
控制转移:执行条件、无条件或者多路分支(switch)。
Control transfer: perform conditional, unconditional, or multiway branches (switch).
方法调用:从类和接口的普通方法和静态方法(包括构造函数和初始化程序)调用和返回。Java 7 JVM 中引入的invokedynamic指令允许在运行时自定义各个调用站点的链接约定。它既可用于Java 8 lambda表达式,也可用于在JVM之上实现动态类型语言。
Method calls: call and return from ordinary and static methods (including constructors and initializers) of classes and interfaces. An invokedynamic instruction, introduced in the Java 7 JVM, allows run-time customization of linkage conventions for individual call sites. It is used both for Java 8 lambda expressions and for the implementation of dynamically typed languages on top of the JVM.
异常: throw ( catch不需要指令)。
Exceptions: throw (no instructions required for catch).
监视器:进入和退出(通过方法调用来调用wait、notify和notifyAll)。
Monitors: enter and exit (wait, notify, and notifyAll are invoked via method calls).
安全性是 Java 语言和虚拟机定义中的主要考虑因素之一。执行从更传统的语言编译的机器代码时可能“出错”的许多事情在执行从 Java 编译的字节码时不会出错。安全性的某些方面是通过限制字节码指令集的表现力或在加载时检查属性来实现的。例如,不能跳转到不存在的地址,因为方法调用通过名称以符号形式指定其目标,而分支目标则被指定为当前方法的代码属性中的索引。同样,当硬件允许从帧指针进行位移寻址以访问当前堆栈帧之外的内存时,JVM 会在加载时进行检查,以确保对局部变量的引用(由局部变量数组中的常量索引指定)在声明的范围内。
Safety was one of the principal concerns in the definition of the Java language and virtual machine. Many of the things that can “go wrong” while executing machine code compiled from a more traditional language cannot go wrong when executing bytecode compiled from Java. Some aspects of safety are obtained by limiting the expressiveness of the byte-code instruction set or by checking properties at load time. One cannot jump to a nonexistent address, for example, because method calls specify their targets symbolically by name, and branch targets are specified as indices within the code attribute of the current method. Similarly, where hardware allows displacement addressing from the frame pointer to access memory outside the current stack frame, the JVM checks at load time to make sure that references to local variables (specified by constant indices into the local variable array) are within the bounds declared.
JVM 在执行期间保证了其他方面的安全性。如果给定了空引用,字段访问和方法调用指令将引发异常。同样,如果索引不在数组的边界内,数组加载和存储指令也会引发异常。
Other aspects of safety are guaranteed by the JVM during execution. Field access and method call instructions throw an exception if given a null reference. Similarly, array load and store instructions throw an exception if the index is not within the bounds of the array.
首次加载类文件时,JVM 会检查文件的顶层结构。除其他事项外,它还会验证文件是否以适当的“魔法数字”开头,文件各个部分的指定大小是否都在界限之内,以及这些大小加起来是否等于整个文件的大小。将类文件链接到程序的其余部分时,JVM 还会检查其他约束。它会验证常量池中的所有项目是否格式正确,并且没有任何东西从最终类继承。更重要的是,它会对类方法的字节码执行一系列检查。除其他事项外,字节码验证器还会确保每个变量在读取之前都已初始化,每个操作都是类型安全的,并且方法的操作数堆栈永远不会溢出或下溢。所有这三项检查都需要数据流分析来确定所需的属性(初始化状态、本地堆栈框架中的插槽类型、操作数堆栈的深度)在程序中给定点的每条可能路径上都是相同的。我们将在 C-17.4 节中更详细地考虑数据流。
When it first loads a class file, the JVM checks the top-level structure of the file. Among other things, it verifies that the file begins with the appropriate “magic number,” that the specified sizes of the various sections of the file are all within bounds, and that these sizes add up to the size of the overall file. When it links the class file into the rest of the program, the JVM checks additional constraints. It verifies that all items in the constant pool are well formed, and that nothing inherits from a final class. More significantly, it performs a host of checks on the bytecode of the class's methods. Among other things, the bytecode verifier ensures that every variable is initialized before it is read, that every operation is type-safe, and that the operand stacks of methods never overflow or underflow. All three of these checks require data flow analysis to determine that desired properties (initialization status, types of slots in the local stack frame, depth of the operand stack) are the same on every possible path to a given point in the program. We will consider data flow in more detail in Section C-17.4.
早在 20 世纪 80 年代中期,微软就认识到在 Windows 平台上运行的编程语言之间需要互操作性。在一系列通过十五年的产品供应,该公司开发了日益复杂的组件对象模型 (COM) 版本,首先可以与用多种语言编写的程序组件进行通信,然后调用,最后共享数据。
As early as the mid-1980s, Microsoft recognized the need for interoperability among programming languages running on Windows platforms. In a series of product offerings spanning a decade and a half, the company developed increasingly sophisticated versions of its Component Object Model (COM), first to communicate with, then to call, and finally to share data with program components written in multiple languages.
随着 Java 的成功,到 20 世纪 90 年代中后期,人们清楚地认识到,将 JVM 风格的运行时系统与 COM 的语言互操作性相结合的系统可能具有巨大的技术和商业潜力。微软的 .NET 项目旨在实现这一潜力。它包括一个类似 JVM 的虚拟机,其规范 — 通用语言基础结构 (CLI) — 由 ECMA 和 ISO 标准化。虽然 CLI 的开发显然是由微软推动的,但其他实现 — 尤其是由 Xamarin, Inc. 领导的开源 Mono 项目 — 也可用于非 Windows 平台。
With the success of Java, it became clear by the mid to late 1990s that a system combining a JVM-style run-time system with the language interoperability of COM could have enormous technical and commercial potential. Microsoft's .NET project set out to realize this potential. It includes a JVM-like virtual machine whose specification—the Common Language Infrastructure (CLI)—is standardized by ECMA and the ISO. While development of the CLI has clearly been driven by Microsoft, other implementations—notably from the open-source Mono project, led by Xamarin, Inc.—are available for non-Windows platforms.
更深入地
IN MORE DEPTH
我们在配套网站上更详细地讨论了 CLI。除其他内容外,我们还描述了通用类型系统(它控制跨语言互操作性)、虚拟机的体系结构(包括其对泛型的支持)、通用中间语言 (CIL)(Java 字节码的 CLI 类似物)以及可移植可执行 (PE)程序集( .jar文件的 CLI 类似物)。
We consider the CLI in more detail on the companion site. Among other things, we describe the Common Type System, which governs cross-language interoperability; the architecture of the virtual machine, including its support for generics; the Common Intermediate Language (CIL—the CLI analogue of Java bytecode); and Portable Executable (PE) assemblies, the CLI analogue of .jar files.
在传统概念中(示例 1.7),编译是一次性活动,与程序执行截然不同。编译器生成目标程序(通常为机器语言),随后可针对许多不同的输入执行多次。
In the traditional conception (Example 1.7), compilation is a one-time activity, sharply distinguished from program execution. The compiler produces a target program, typically in machine language, which can subsequently be executed many times for many different inputs.
但在某些环境中,将编译和执行在时间上缩短在一起是有意义的。即时(JIT) 编译器在每次运行程序之前立即将程序从源代码或中间形式转换为机器语言。我们将在下面的第一小节中进一步考虑 JIT 编译。我们还考虑了在程序开始执行后可能会编译程序新部分或重新编译旧部分的语言系统。在第16.2.2和16.2.3节中,我们考虑了二进制翻译和二进制重写系统,它们无需访问源代码即可对程序执行类似编译器的操作。最后,在第 16.2.4 节中,我们考虑了可以从远程位置下载程序组件的系统。所有这些系统都用于延迟程序与其机器代码的绑定。
In some environments, however, it makes sense to bring compilation and execution closer together in time. A just-in-time (JIT) compiler translates a program from source or intermediate form into machine language immediately before each separate run of the program. We consider JIT compilation further in the first subsection below. We also consider language systems that may compile new pieces of a program—or recompile old pieces—after the program begins its execution. In Sections 16.2.2 and 16.2.3, we consider binary translation and binary rewriting systems, which perform compiler-like operations on programs without access to source code. Finally, in Section 16.2.4, we consider systems that may download program components from remote locations. All these systems serve to delay the binding of a program to its machine code.
为了推广 Java 语言和虚拟机,Sun Microsystems 提出了“一次编写,随处运行”的口号,其理念是让以 Java 字节码形式分发的程序可以在各种各样的平台上运行。当然,源代码也是可移植的,但字节码紧凑得多,并且可以在不需要额外预处理的情况下进行解释。不幸的是,解释的开销往往很大。在早期 Java 实现上运行的程序可能比其他语言中编译后的代码慢一个数量级。即时编译是一种在提高执行速度的同时保留字节码可移植性的技术。与解释和动态链接(第 15.7 节)一样,JIT 编译也受益于程序组件的延迟发现:程序代码不会因广泛共享的库的副本而变得臃肿,并且在运行需要库的程序时会自动获取新版本的库。
To promote the Java language and virtual machine, Sun Microsystems coined the slogan “write once, run anywhere”—the idea being that programs distributed as Java bytecode could run on a very wide range of platforms. Source code, of course, is also portable, but byte code is much more compact, and can be interpreted without additional preprocessing. Unfortunately, interpretation tends to be expensive. Programs running on early Java implementations could be as much as an order of magnitude slower than compiled code in other languages. Just-in-time compilation is, to first approximation, a technique to retain the portability of bytecode while improving execution speed. Like both interpretation and dynamic linking (Section 15.7), JIT compilation also benefits from the delayed discovery of program components: program code is not bloated by copies of widely shared libraries, and new versions of libraries are obtained automatically when a program that needs them is run.
由于 JIT 系统在执行之前立即编译程序,因此它可能会显著延迟程序的启动时间。实现者面临着一个艰难的权衡:为了最大限度地提高解释方面的效益,编译器应该生成良好的代码;为了最大限度地缩短启动时间,它应该非常快速地生成代码。一般来说,JIT 编译器倾向于专注于更简单的目标代码改进形式。具体来说,它们通常将自己限制在所谓的局部改进上,这些改进在单个控制流构造内运行。考虑全局(整个方法)和过程间(整个程序)级别的改进可能代价高昂。
Because a JIT system compiles programs immediately prior to execution, it can add significant delay to program start-up time. Implementors face a difficult tradeoff: to maximize benefits with respect to interpretation, the compiler should produce good code; to minimize start-up time, it should produce that code very quickly. In general, JIT compilers tend to focus on the simpler forms of target code improvement. Specifically, they often limit themselves to the so-called local improvements, which operate within individual control-flow constructs. Improvements at the global (whole method) and interprocedural (whole program) level may be expensive to consider.
幸运的是,由于存在较早的源代码到字节码编译器,JIT 编译的成本通常会降低,因为该编译器可以完成大部分“繁重工作”。5 JIT编译器不需要扫描,因为字节码不是文本。解析很简单,因为类文件具有简单的自描述结构。源代码到字节码编译器必须以很大的代价推断的许多属性(类型安全、实际和形式参数列表的一致性)直接嵌入在字节码的结构中(对象用其类型标记,调用通过方法描述符进行);其他属性可以通过简单的数据流分析进行验证。源代码到字节码编译器还可以执行某些形式的独立于机器的代码改进(这些改进在一定程度上受到基于堆栈的表达式评估的限制)。
Fortunately, the cost of JIT compilation is typically lessened by the existence of an earlier source-to-byte-code compiler that does much of the “heavy lifting.”5 Scanning is unnecessary in a JIT compiler, since bytecode is not textual. Parsing is trivial, since class files have a simple, self-descriptive structure. Many of the properties that a source-to-byte-code compiler must infer at significant expense (type safety, agreement of actual and formal parameter lists) are embedded directly in the structure of the bytecode (objects are labeled with their type, calls are made through method descriptors); others can be verified with simple data flow analysis. Certain forms of machine-independent code improvement may also be performed by the source-to-byte-code compiler (these are limited to some degree by stack-based expression evaluation).
所有这些因素使得 JIT 编译器比人们最初预期的运行速度更快,并且生成的代码质量更高。此外,由于我们已经承诺在运行时调用 JIT 编译器,因此我们可以通过一次运行一点而不是一次性运行来最大限度地减少其对程序启动延迟的影响:
All these factors allow a JIT compiler to be faster—and to produce better code—than one might initially expect. In addition, since we are already committed to invoking the JIT compiler at run time, we can minimize its impact on program start-up latency by running it a bit at a time, rather than all at once:
■ 与惰性链接器(第 C-15.7.2 节)类似,JIT 编译器可以逐步执行其工作。它首先只编译包含程序入口点(即main)的类文件,在代码中留下钩子,当程序应该调用另一个类文件中的方法时,这些钩子会调用运行时系统。经过这一小段准备后,程序开始执行。当执行通过未解析的钩子进入运行时时,运行时将调用编译器来加载新的类文件并将其链接到程序中。
■ Like a lazy linker (Section C-15.7.2), a JIT compiler may perform its work incrementally. It begins by compiling only the class file that contains the program entry point (i.e., main), leaving hooks in the code that call into the run-time system wherever the program is supposed to call a method in another class file. After this small amount of preparation, the program begins execution. When execution falls into the runtime through an unresolved hook, the runtime invokes the compiler to load the new class file and to link it into the program.
■ 为了消除编译原始类文件的延迟,语言实现可能同时包含解释器和 JIT 编译器。执行从解释器开始。同时,编译器将程序的各部分翻译成机器码。当解释器需要调用方法时,它会检查编译版本是否可用,如果有,则调用该版本而不是解释字节码。我们将在下文中在 HotSpot Java 编译器和 JVM 的上下文中回顾这项技术。
■ To eliminate the latency of compiling even the original class file, the language implementation may incorporate both an interpreter and a JIT compiler. Execution begins in the interpreter. In parallel, the compiler translates portions of the program into machine code. When the interpreter needs to call a method, it checks to see whether a compiled version is available yet, and if so calls that version instead of interpreting the bytecode. We will return to this technique below, in the context of the HotSpot Java compiler and JVM.
■ 当类文件经过 JIT 编译时,语言实现可以缓存生成的机器代码以供以后使用。这相当于推测程序当前运行中使用的库例程版本在程序再次运行时仍将是最新的。由于 Java 和 C# 等语言需要库例程的后期绑定,因此必须在每次后续运行中检查此猜测。如果检查成功,使用缓存副本几乎可以节省 JIT 编译的全部成本。
■ When a class file is JIT compiled, the language implementation can cache the resulting machine code for later use. This amounts to guessing, speculatively, that the versions of library routines employed in the current run of the program will still be current when the program is run again. Because languages like Java and C# require the appearance of late binding of library routines, this guess must be checked in each subsequent run. If the check succeeds, using a cached copy saves almost the entire cost of JIT compilation.
最后,JIT 编译提供了执行某些类型的代码改进的机会,而这些改进在传统编译器中通常是不可行的。例如,软件供应商通常会发布一个编译版本的对于给定的指令集架构,JIT 编译器可以针对特定应用程序优化,即使该架构的实现可能在重要方面有所不同,包括管道宽度和深度、物理(重命名)寄存器的数量以及各级缓存的数量、大小和速度。JIT 编译器可能能够识别其所运行的处理器实现,并生成针对该特定实现进行调整的代码。更重要的是,JIT 编译器可能能够内联对动态链接库例程的调用。这种优化在面向对象的程序中尤为重要,因为面向对象的程序往往会调用许多小方法。对于此类程序,动态内联会对性能产生巨大影响。
Finally, JIT compilation affords the opportunity to perform certain kinds of code improvement that are usually not feasible in traditional compilers. It is customary, for example, for software vendors to ship a single compiled version of an application for a given instruction set architecture, even though implementations of that architecture may differ in important ways, including pipeline width and depth; the number of physical (renaming) registers; and the number, size, and speed of the various levels of cache. A JIT compiler may be able to identify the processor implementation on which it is running, and generate code that is tuned for that specific implementation. More important, a JIT compiler may be able to in-line calls to dynamically linked library routines. This optimization is particularly important in object-oriented programs, which tend to call many small methods. For such programs, dynamic in-lining can have a dramatic impact on performance.
我们注意到,语言实现可能会选择延迟 JIT 编译,以减少对程序启动延迟的影响。在某些情况下,必须延迟编译,因为源代码或字节码直到运行时才创建或发现,或者因为我们希望执行依赖于执行期间收集的信息的优化。在这些情况下,我们说语言实现采用了动态编译。Common Lisp 系统已经使用动态编译很多年了:语言通常是编译的,但程序可以在运行时扩展自身。基于运行时统计的优化是一项较新的创新。
We have noted that a language implementation may choose to delay JIT compilation to reduce the impact on program start-up latency. In some cases, compilation must be delayed, either because the source or bytecode was not created or discovered until run time, or because we wish to perform optimizations that depend on information gathered during execution. In these cases, we say the language implementation employs dynamic compilation. Common Lisp systems have used dynamic compilation for many years: the language is typically compiled, but a program can extend itself at run time. Optimization based on run-time statistics is a more recent innovation.
大多数程序的大部分时间都花在代码的一小部分上。对这一部分进行积极的代码改进可以大大提高程序性能。动态编译器可以使用运行时分析收集的统计数据来识别代码中的热路径,然后在后台对其进行优化。通过重新排列代码以使热路径在内存中连续,它还可以提高指令缓存的性能。额外的运行时统计数据可能会建议展开循环(练习 C-5.21)、将常用表达式分配给寄存器(第 C-5.5.2 和 C-17.8 节)以及调度指令以最大限度地减少流水线停顿(第 C-5.5.1 和 C-17.6 节)的机会。
Most programs spend most of their time in a relatively small fraction of the code. Aggressive code improvement on this fraction can yield disproportionately large improvements in program performance. A dynamic compiler can use statistics gathered by run-time profiling to identify hot paths through the code, which it then optimizes in the background. By rearranging the code to make hot paths contiguous in memory, it may also improve the performance of the instruction cache. Additional run-time statistics may suggest opportunities to unroll loops (Exercise C-5.21), assign frequently used expressions to registers (Sections C-5.5.2 and C-17.8), and schedule instructions to minimize pipeline stalls (Sections C-5.5.1 and C-17.6).
HotSpot 是 Oracle 针对桌面和服务器系统的主要 JVM 和 JIT 编译器。它于 1999 年首次发布,并以开源形式提供。
HotSpot is Oracle's principal JVM and JIT compiler for desktop and server systems. It was first released in 1999, and is available as open source.
HotSpot 的名称源于它使用动态编译来提高热代码路径的性能。新加载的类文件最初被解释。JVM 选择经常执行的方法进行编译,随后动态修补到程序中。编译器会积极地内联小例程,并将以深度、迭代的方式进行,反复内联从刚完成内联的代码中调用的例程。如前面关于动态编译的讨论中所述,编译器还将内联仅对当前类文件集安全的例程,并将动态“取消优化”由于加载新的派生类而变得不安全的内联调用。
HotSpot takes its name from its use of dynamic compilation to improve the performance of hot code paths. Newly loaded class files are initially interpreted. Methods that are executed frequently are selected by the JVM for compilation and are subsequently patched into the program on the fly. The compiler is aggressive about in-lining small routines, and will do so in a deep, iterative fashion, repeatedly in-lining routines that are called from the code it just finished in-lining. As described in the preceding discussion of dynamic compilation, the compiler will also in-line routines that are safe only for the current set of class files, and will dynamically “deoptimize” in-lined calls that have been rendered unsafe by the loading of new derived classes.
HotSpot 编译器可以配置为在“客户端”或“服务器”模式下运行。客户端模式针对较低的启动延迟进行了优化。它适用于人类用户经常启动新程序的系统。它将 Java 字节码转换为静态单赋值 (SSA) 形式(第 C-17.4.1 节中描述的中级 IF),并执行一些简单的与机器无关的优化。然后,它转换为低级 IF,并对其执行指令调度和寄存器分配。最后,它将这个IF翻译成机器码。
The HotSpot compiler can be configured to operate in either “client” or “server” mode. Client mode is optimized for lower start-up latency. It is appropriate for systems in which a human user frequently starts new programs. It translates Java bytecode to static single assignment (SSA) form (a medium-level IF described in Section C-17.4.1) and performs a few straightforward machine-independent optimizations. It then translates to a low-level IF, on which it performs instruction scheduling and register allocation. Finally, it translates this IF to machine code.
服务器模式经过优化,可生成更快的代码。它适用于需要最大吞吐量且可以容忍较慢启动速度的系统。它将大多数经典的全局和过程间代码改进技术应用于程序的 SSA 版本(其中许多技术在第17 章中描述),以及其他特定于 Java 的改进。其中许多改进都利用了分析统计数据。
Server mode is optimized to generate faster code. It is appropriate for systems that need maximum throughput and can tolerate slower start-up. It applies most classic global and interprocedural code improvement techniques to the SSA version of the program (many of these are described in Chapter 17), as well as other improvements specific to Java. Many of these improvements make use of profiling statistics.
特别是在服务器模式下运行时,HotSpot 的性能可与传统的 C 和 C++ 编译器相媲美。实际上,积极的内联和配置文件驱动的优化可以“挽回” JIT 编译的启动延迟和 Java 运行时语义检查的开销。
Particularly when running in server mode, HotSpot can rival the performance of traditional compilers for C and C++. In effect, aggressive in-lining and profile-driven optimization serve to “buy back” both the start-up latency of JIT compilation and the overhead of Java's run-time semantic checks.
即时编译器和动态编译器假设源代码或字节码的可用性,这些代码保留了源代码的所有语义信息。然而,有时重新编译目标代码会很有用。这个过程称为二进制翻译。它允许已编译的程序在具有不同指令集体系结构的机器上运行。一些读者可能还记得Apple 的 Rosetta 系统允许为基于 PowerPC 的旧 Macintosh 计算机编译的程序在基于 x86 的较新 Mac 上运行。Rosetta 建立在大量类似翻译器的经验基础之上。
Just-in-time and dynamic compilers assume the availability of source code or of bytecode that retains all of the semantic information of the source. There are times, however, when it can be useful to recompile object code. This process is known as binary translation. It allows already-compiled programs to be run on a machine with a different instruction set architecture. Some readers may recall Apple's Rosetta system, which allowed programs compiled for older PowerPC-based Macintosh computers to run on newer x86-based Macs. Rosetta built on experience with a long line of similar translators.
二进制翻译的主要挑战是原始源代码到目标代码的翻译中信息的丢失。目标代码通常缺少类型信息以及源代码和字节码中明确界定的子例程和控制流结构。虽然大部分此类信息都出现在编译器的符号表中,有时可能包含在目标文件中以用于调试目的,但供应商通常会在发布商业产品之前将其删除,二进制翻译器无法假设这些信息会存在。
The principal challenge for binary translation is the loss of information in the original source-to-object-code translation. Object code typically lacks both type information and the clearly delineated subroutines and control-flow constructs of source code and bytecode. While most of this information appears in the compiler's symbol table, and may sometimes be included in the object file for debugging purposes, vendors usually delete it before shipping commercial products, and a binary translator cannot assume it will be present.
典型的二进制翻译器会读取目标文件并重建第 15.1.1 节中描述的控制流图。由于缺乏有关基本块的明确信息,这项任务变得复杂。虽然分支(基本块的末尾)很容易识别,但开头却比较困难:由于分支目标有时是在运行时计算的,或者在调度表或虚函数表中查找,所以二进制翻译器必须考虑到控制权有时可能会跳转到“可能的基本”块中间的可能性。由于翻译后的代码通常不会位于与原始代码相同的地址,因此必须将计算出的分支翻译成执行某种表查找的代码,或者依靠解释。
The typical binary translator reads an object file and reconstructs a control flow graph of the sort described in Section 15.1.1. This task is complicated by the lack of explicit information about basic blocks. While branches (the ends of basic blocks) are easy to identify, beginnings are more difficult: since branch targets are sometimes computed at run time or looked up in dispatch tables or virtual function tables, the binary translator must consider the possibility that control may sometimes jump into the middle of a “probably basic” block. Since translated code will generally not lie at the same address as the original code, computed branches must be translated into code that performs some sort of table lookup, or falls back on interpretation.
对于任意目标代码,静态二进制翻译并不总是可行的。除了计算分支之外,问题还包括自修改代码(写入其自身指令空间的程序)、动态生成的代码(例如,如示例 C-9.61 中所述的单指针闭包)以及各种形式的自省,其中程序检查并推理其自身状态(我们将在16.3 节中更全面地考虑这一点)。幸运的是,许多常见的习语可以被识别并作为特殊情况处理,对于无法静态处理的(相对罕见的)情况,二进制翻译器总是可以将某些翻译延迟到运行时,转而使用解释,或者只是通知用户无法进行翻译。在实践中,二进制翻译已被证明非常成功。
Static binary translation is not always possible for arbitrary object code. In addition to computed branches, problems include self-modifying code (programs that write to their own instruction space), dynamically generated code (e.g., for single-pointer closures, as described in Example C-9.61), and various forms of introspection, in which a program examines and reasons about its own state (we will consider this more fully in Section 16.3). Fortunately, many common idioms can be identified and treated as special cases, and for the (comparatively rare) cases that can't be handled statically, a binary translator can always delay some translation until run time, fall back on interpretation, or simply inform the user that translation is not possible. In practice, binary translation has proved remarkably successful.
在长期运行的程序中,动态翻译器可能会重新访问热路径并更积极地优化它们。类似的策略也可以应用于不需要翻译的程序,即已经作为底层架构的机器代码存在的程序。据报道,通过利用运行时分析信息,这种动态优化可以使性能比已优化的代码提高 20%。
In a long-running program, a dynamic translator may revisit hot paths and optimize them more aggressively. A similar strategy can also be applied to programs that don't need translation—that is, to programs that already exist as machine code for the underlying architecture. This sort of dynamic optimization has been reported to improve performance by as much as 20% over already-optimized code, by exploiting run-time profiling information.
虽然二进制优化器的目标是在不改变程序行为的情况下提高程序性能,但人们也可以想象出一些旨在改变该行为的工具。二进制重写是一种修改现有可执行程序的通用技术,通常用于插入某种类型的检测。最常见的检测形式是收集性能分析信息。例如,人们可以计算每个子例程被调用的次数,或者每个循环迭代的次数(练习 16.5)。这些计数可以存储在内存的缓冲区中,并在执行结束时转储。或者,人们可以记录所有内存引用。这样的日志通常需要在程序运行时发送到文件中——因为它太长了,内存无法容纳。
While the goal of a binary optimizer is to improve the performance of a program without altering its behavior, one can also imagine tools designed to change that behavior. Binary rewriting is a general technique to modify existing executable programs, typically to insert instrumentation of some kind. The most common form of instrumentation collects profiling information. One might count the number of times that each subroutine is called, for example, or the number of times that each loop iterates (Exercise 16.5). Such counts can be stored in a buffer in memory, and dumped at the end of execution. Alternatively, one might log all memory references. Such a log will generally need to be sent to a file as the program runs—it will be too long to fit in memory.
除了分析之外,二进制重写还可用于
In addition to profiling, binary rewriting can be used to
■ 模拟新的架构:模拟器感兴趣的操作被跳入特殊运行时库的代码替换(其他代码以本机速度运行)。
■ Simulate new architectures: operations of interest to the simulator are replaced with code that jumps into a special run-time library (other code runs at native speed).
■ 通过识别一系列测试未探索的代码路径来评估测试套件的覆盖率。
■ Evaluate the coverage of test suites, by identifying paths through the code that are not explored by a series of tests.
■ 实现并行程序的模型检查,该过程通过强制程序在不同线程中交错执行操作来暴露竞争条件(示例 13.2 )。
■ Implement model checking for parallel programs, a process that exposes race conditions (Example 13.2) by forcing a program through different interleavings of operations in different threads.
■ “审计”编译器优化的质量。例如,可以检查加载到寄存器中的值是否始终与已经存在的值相同(此类加载表明编译器可能未能意识到加载是多余的)。
■ “Audit” the quality of a compiler's optimizations. For example, one might check whether the value loaded into a register is always the same as the value that was already there (such loads suggest that the compiler may have failed to realize that the load was redundant).
■ 在缺少动态语义检查的程序中插入动态语义检查。二进制重写不仅可用于简单的检查,如空指针取消引用和算术溢出,还可用于各种内存访问错误,包括未初始化的变量、悬空引用、内存泄漏、“双重删除”(试图释放已释放的内存块)以及访问动态分配数组的末尾。
■ Insert dynamic semantic checks into a program that lacks them. Binary rewriting can be used not only for simple checks like null-pointer dereference and arithmetic overflow, but for a wide variety of memory access errors as well, including uninitialized variables, dangling references, memory leaks, “double deletes” (attempts to deallocate an already deallocated block of memory), and access off the ends of dynamically allocated arrays.
更雄心勃勃的是,如侧边栏 16.5 中所述,二进制重写可用于“沙盒化”不受信任的代码,以便它可以安全地在与应用程序其余部分相同的地址空间中执行。
More ambitiously, as described in Sidebar 16.5, binary rewriting can be used to “sandbox” untrusted code so that it can safely be executed in the same address space as the rest of the application.
对于现代处理器而言,ATOM 已被 Pin 取代,Pin 是英特尔研究人员在 21 世纪初开发的二进制重写器,并以开源形式发布。Pin 的设计在很大程度上独立于机器,不仅适用于 x86、x86-64 和 Itanium,还适用于 ARM。
For modern processors, ATOM has been supplanted by Pin, a binary rewriter developed by researchers at Intel in the early 2000s, and distributed as open source. Designed to be largely machine independent, Pin is available not only for the x86, x86-64, and Itanium, but also for ARM.
Pin 直接受到 ATOM 的启发,并具有类似的编程接口。特别是,它保留了检测和分析例程的概念。它还借鉴了 Dynamo 和其他动态翻译工具的思想。最重要的是,它使用 Dynamo 跟踪机制的扩展版本在运行时检测以前未修改的程序;程序在磁盘上的表示永远不会改变。Pin 甚至可以附加到已经运行的应用程序,就像我们将在第 16.3.2 节中研究的符号调试器一样。
Pin was directly inspired by ATOM, and has a similar programming interface. In particular, it retains the notions of instrumentation and analysis routines. It also borrows ideas from Dynamo and other dynamic translation tools. Most significantly, it uses an extended version of Dynamo's trace mechanism to instrument previously unmodified programs at run time; the on-disk representation of the program never changes. Pin can even be attached to an already-running application, much like the symbolic debuggers we will study in Section 16.3.2.
与 Dynamo 一样,Pin 首先将基本块的初始跟踪写入运行时跟踪缓存。当它到达无条件分支、预定义的最大条件分支数或预定义的最大指令数时,它会结束跟踪。在写入时,它会在代码中的适当位置插入对分析例程(或短例程的内联版本)的调用。它还维护原始程序地址和跟踪中的地址之间的映射,因此它可以相应地修改特定于地址的指令。一旦它完成创建跟踪,Pin 就会跳转到其第一条指令。退出跟踪的条件分支设置为链接到其他跟踪,或跳回 Pin。
Like Dynamo, Pin begins by writing an initial trace of basic blocks into a runtime trace cache. It ends the trace when it reaches an unconditional branch, a predefined maximum number of conditional branches, or a predefined maximum number of instructions. As it writes, it inserts calls to analysis routines (or in-line versions of short routines) at appropriate places in the code. It also maintains a mapping between original program addresses and addresses in the trace, so it can modify address-specific instructions accordingly. Once it has finished creating a trace, Pin simply jumps to its first instruction. Conditional branches that exit the trace are set to link to other traces, or to jump back into Pin.
间接分支的处理尤为谨慎。基于运行时分析,Pin 维护一组此类分支目标的预测,按最有可能的顺序排列。每个预测都包含原始程序中的地址(用作键)和跟踪缓存中要跳转到的地址。如果所有预测均不匹配,Pin 会在其原始和跟踪缓存地址之间的映射中返回表查找。如果仍未找到匹配项,Pin 会返回指令集解释器,从而使其能够处理动态生成的代码。
Indirect branches are handled with particular care. Based on run-time profiling, Pin maintains a set of predictions for the targets of such branches, sorted most likely first. Each prediction consists of an address in the original program (which serves as a key) and an address to jump to in the trace cache. If none of the predictions match, Pin falls back to table lookup in its mapping between original and trace cache addresses. If match is still not found, Pin falls back on an instruction set interpreter, allowing it to handle even dynamically generated code.
为了减少调用分析例程时保存寄存器的需要,并促进这些例程的内联扩展,Pin 为每个跟踪的指令执行自己的寄存器分配,尽可能对相互链接的跟踪使用类似的分配。在多线程程序中,一个寄存器被静态保留以指向线程特定的缓冲区,必要时寄存器可以溢出。除非之后需要它们的值,否则不会在调用分析例程时保存条件代码。对于可以在基本块内任何地方调用的例程,Pin 会寻找保存和恢复成本最小的位置。
To reduce the need to save registers when calling analysis routines, and to facilitate in-line expansion of those routines, Pin performs its own register allocation for the instructions of each trace, using similar allocations whenever possible for traces that link to one another. In multithreaded programs, one register is statically reserved to point to a thread-specific buffer, where registers can be spilled when necessary. Condition codes are not saved across calls to analysis routines unless their values are needed afterward. For routines that can be called anywhere within a basic block, Pin hunts for a location where the cost of saving and restoring is minimized.
可移植性是后期绑定机器代码的主要动机之一。为一种机器架构或操作系统编译的代码通常不能在另一种机器架构或操作系统上运行。但是,字节码(Java 字节码,CIL)或脚本语言(JavaScript、Visual Basic)中的代码紧凑且独立于机器:它可以轻松地在互联网上移动并在几乎任何平台上运行。这种移动代码越来越普遍。每个主流浏览器都支持 JavaScript;大多数浏览器还允许执行 Java 小程序。Visual Basic 宏通常不仅嵌入在用于使用 Internet Explorer 查看的页面中,还嵌入在通过电子邮件分发的 Excel、Word 和 Outlook 文档中。手机应用程序可以使用移动代码来分发在现有进程中运行的游戏、生产力工具和交互式媒体。
Portability is one of the principal motivations for late binding of machine code. Code that has been compiled for one machine architecture or operating system cannot generally be run on another. Code in a byte code (Java bytecode, CIL) or scripting language (JavaScript, Visual Basic), however, is compact and machine independent: it can easily be moved over the Internet and run on almost any platform. Such mobile code is increasingly common. Every major browser supports JavaScript; most enable the execution of Java applets as well. Visual Basic macros are commonly embedded not only in pages meant for viewing with Internet Explorer, but also in Excel, Word, and Outlook documents distributed via email. Cell phone apps may use mobile code to distribute games, productivity tools, and interactive media that run within an existing process.
从某种意义上说,移动代码并不是什么新鲜事:我们几乎所有的软件都来自其他来源;我们通过互联网下载软件,或者从 DVD 上安装软件。从历史上看,这种使用模式依赖于信任(我们假设知名公司的软件是安全的)以及安装的明确性和偶然性。近年来,情况发生了变化,人们希望频繁下载代码,从可能不受信任的来源下载,而且通常用户没有意识到。
In some sense, mobile code is nothing new: almost all our software comes from other sources; we download it over the Internet or perhaps install it from a DVD. Historically, this usage model has relied on trust (we assume that software from a well-known company will be safe) and on the very explicit and occasional nature of installation. What has changed in recent years is the desire to download code frequently, from potentially untrusted sources, and often without the conscious awareness of the user.
移动代码具有多种风险。它可能会访问和泄露机密信息(间谍软件)。它可能会以令人讨厌的方式干扰计算机的正常使用(广告软件)。它可能会损坏现有程序或数据,或保存自身副本并在用户不知情的情况下运行(各种恶意软件)。特别是在严重的情况下,它可能会把主机当作“僵尸”来对其他用户发起攻击。
Mobile code carries a variety of risks. It may access and reveal confidential information (spyware). It may interfere with normal use of the computer in annoying ways (adware). It may damage existing programs or data, or save copies of itself that run without the user's intent (malware of various kinds). In particular egregious cases, it may use the host machine as a “zombie” from which to launch attacks on other users.
为了防止意外和恶意行为,必须在某种沙盒中执行移动代码,如边栏 14.6 中所述。沙盒的创建很困难,因为必须保护的资源种类繁多。至少,需要监视或限制对处理器周期、代码自身指令和数据之外的内存、文件系统、网络接口、其他设备(例如,密码可能通过窥探键盘而被盗)、窗口系统(例如,禁用弹出广告)以及操作系统提供的任何其他潜在危险服务的访问。
To protect against unwanted behavior, both accidental and malicious, mobile code must be executed in some sort of sandbox, as described in Sidebar 14.6. Sandbox creation is difficult because of the variety of resources that must be protected. At a minimum, one needs to monitor or limit access to processor cycles, memory outside the code's own instructions and data, the file system, network interfaces, other devices (passwords, for example, may be stolen by snooping the keyboard), the window system (e.g., to disable pop-up ads), and any other potentially dangerous services provided by the operating system.
沙盒机制位于语言实现和操作系统之间的边界。传统上,操作系统提供的虚拟内存技术可用于限制对内存的访问,但对于许多形式的移动代码而言,这通常过于昂贵。当今最常见的两种技术(均依赖于本章讨论的技术)是二进制重写和在非信任解释器中执行。这两种情况都因安全性和实用性之间的内在矛盾而变得复杂:我们允许非信任代码执行的操作越少,它的用处就越小。没有一种策略可能适用于所有情况。如果小程序只能操作窗口中的图像,那么它们可能是完全安全的,但嵌入在电子表格中的宏可能无法在不更改用户数据的情况下完成其工作。未来工作的主要挑战是找到一种方法来帮助用户(他们不能理解技术细节)做出明智的决定,决定在移动代码中允许什么和不允许什么。
Sandboxing mechanisms lie at the boundary between language implementation and operating systems. Traditionally, OS-provided virtual memory techniques might be used to limit access to memory, but this is generally too expensive for many forms of mobile code. The two most common techniques today—both of which rely on technology discussed in this chapter—are binary rewriting and execution in an untrusting interpreter. Both cases are complicated by an inherent tension between safety and utility: the less we allow untrusted code to do, the less useful it can be. No single policy is likely to work in all cases. Applets may be entirely safe if all they can do is manipulate the image in a window, but macros embedded in a spreadsheet may not be able to do their job without changing the user's data. A major challenge for future work is to find a way to help users— who cannot be expected to understand the technical details—to make informed decisions about what and what not to allow in mobile code.
符号表元数据使实用程序(即时和动态编译器、优化器、调试器、分析器和二进制重写器)可以轻松检查程序并推断其结构和类型。我们将在第 16.3.2和16.3.3节中特别考虑调试器和分析器。但是,没有理由将元数据的使用仅限于外部工具,事实上也并非如此:Lisp 长期以来一直允许程序推断其自身的内部结构和类型(这种推理有时称为自省)。Java 和 C# 通过反射API提供类似的功能,该 API 允许程序仔细阅读其自己的元数据。反射也出现在其他几种语言中,包括 Prolog(侧边栏 12.2)和所有主要的脚本语言。在动态类型语言(如 Lisp)中,反射是必不可少的:它允许库或应用程序函数对其自己的参数进行类型检查。在静态类型语言中,反射支持各种传统上不可行的编程习惯用法。
Symbol table metadata makes it easy for utility programs—just-in-time and dynamic compilers, optimizers, debuggers, profilers, and binary rewriters—to inspect a program and reason about its structure and types. We consider debuggers and profilers in particular in Sections 16.3.2 and 16.3.3. There is no reason, however, why the use of metadata should be limited to outside tools, and indeed it is not: Lisp has long allowed a program to reason about its own internal structure and types (this sort of reasoning is sometimes called introspection). Java and C# provide similar functionality through a reflection API that allows a program to peruse its own metadata. Reflection appears in several other languages as well, including Prolog (Sidebar 12.2) and all the major scripting languages. In a dynamically typed language such as Lisp, reflection is essential: it allows a library or application function to type check its own arguments. In a statically typed language, reflection supports a variety of programming idioms that were not traditionally feasible.
更重要的是,反射在操纵其他程序的程序中很有用。例如,大多数程序开发环境都有组织和“漂亮地打印”程序的类、方法和变量的机制。在具有反射的语言中,这些工具无需检查源代码:如果它们将已编译的程序加载到自己的地址空间中,它们可以使用反射 API 来查询编译器创建的符号表信息。解释器、调试器和分析器可以以类似的方式工作。在分布式系统中,程序可以使用反射来创建通用的序列化机制,能够将几乎任意的结构转换为可以通过网络发送并在另一端重新组装的线性字节流。(Java 和 C# 都在其标准库中包含此类机制,这些机制是在基本语言之上实现的。)在日益动态的在互联网应用世界中,人们甚至可以创建约定,让程序“查询”新发现的对象,看它实现了哪些方法,然后选择调用其中的方法。
More significantly, reflection is useful in programs that manipulate other programs. Most program development environments, for example, have mechanisms to organize and “pretty-print” the classes, methods, and variables of a program. In a language with reflection, these tools have no need to examine source code: if they load the already-compiled program into their own address space, they can use the reflection API to query the symbol table information created by the compiler. Interpreters, debuggers, and profilers can work in a similar fashion. In a distributed system, a program can use reflection to create a general-purpose serialization mechanism, capable of transforming an almost arbitrary structure into a linear stream of bytes that can be sent over a network and reassembled at the other end. (Both Java and C# include such mechanisms in their standard library, implemented on top of the basic language.) In the increasingly dynamic world of Internet applications, one can even create conventions by which a program can “query” a newly discovered object to see what methods it implements, and then choose which of these to call.
当然,不加约束地使用反射会带来危险。由于反射允许应用程序窥视类的实现(例如,列出其私有成员),因此它违反了抽象和信息隐藏的正常规则。某些安全策略可能会禁用它(例如,在沙盒环境中)。通过限制目标代码与源代码的差异程度,它可能会阻止某些形式的代码改进。
There are dangers, of course, associated with the undisciplined use of reflection. Because it allows an application to peek inside the implementation of a class (e.g., to list its private members), reflection violates the normal rules of abstraction and information hiding. It may be disabled by some security policies (e.g., in sandboxed environments). By limiting the extent to which target code can differ from the source, it may preclude certain forms of code improvement.
给定一个Class对象c,可以调用c.getSuperclass()来获取c的父类的Class对象。类似地,c.getClasses()将返回一个Class对象数组,每个对象对应于c的类中声明的每个公共类。也许更有趣的是,c.getMethods()、c.getFields()和c.getConstructors()将返回表示所有c的公共方法、字段和构造函数(包括从祖先类继承的方法、字段和构造函数)的对象数组。这些数组的元素分别是Method、Field和Constructor类的实例。它们在包java.lang.reflect中声明,作用类似于 Class 。这些类的许多方法允许查询 Java 类型系统的几乎任何方面,包括修饰符(static 、 private 、 final 、 abstract等)、泛型的类型参数(但不是泛型实例的类型参数 - 这些参数已被删除)、类实现的接口、方法抛出的异常等等。最引人注目的、无法通过 Java 反射 API 获取的东西可能是实现方法的字节码。然而,即使是这个,也可以使用第三方工具进行检查,例如 Apache 字节码工程库 (BCEL) 或 ObjectWeb 的 ASM,这两个工具都是开源的。
Given a Class object c, one can call c.getSuperclass() to obtain a Class object for c's parent. In a similar vein, c.getClasses() will return an array of Class objects, one for each public class declared within c's class. Perhaps more interesting, c.getMethods(), c.getFields(), and c.getConstructors() will return arrays of objects representing all c's public methods, fields, and constructors (including those inherited from ancestor classes). The elements of these arrays are instances of classes Method, Field, and Constructor, respectively. These are declared in package java.lang.reflect, and serve roles analogous to that of Class. The many methods of these classes allow one to query almost any aspect of the Java type system, including modifiers (static, private, final, abstract, etc.), type parameters of generics (but not of generic instances—those are erased), interfaces implemented by classes, exceptions thrown by methods, and much more. Perhaps the most conspicuous thing that is not available through the Java reflection API is the bytecode that implements methods. Even this, however, can be examined using third-party tools such as the Apache Byte Code Engineering Library (BCEL) or ObjectWeb's ASM, both of which are open source.
C# 的反射 API 与 Java 的类似:System.Type类似于java.lang.Class;System.Reflection类似于java.lang.reflect。伪函数typeof充当 Java 的伪字段.class的角色。更实质性的差异源于 PE 程序集包含的信息比 Java 类文件中的信息多一点。例如,我们可以在 C# 中请求形式参数的名称,而不仅仅是它们的类型。更重要的是,对泛型使用具体化而不是擦除意味着我们可以检索用于实例化给定对象的类型参数的精确信息。也许最大的区别是 .NET 提供了一个标准库System.Reflection.Emit来创建 PE 程序集并使用 CIL 填充它们。Reflection.Emit 的功能大致相当于上一小节中提到的 BCEL 和 ASM 工具的功能。但是,由于它是标准库的一部分,因此它可用于在 CLI 上运行的任何程序。
C#'s reflection API is similar to that of Java: System.Type is analogous to java.lang.Class; System.Reflection is analogous to java.lang.reflect. The pseudo function typeof plays the role of Java's pseudo field .class. More substantive differences stem from the fact that PE assemblies contain a bit more information than is found in Java class files. We can ask for names of formal parameters in C#, for example, not just their types. More significantly, the use of reification instead of erasure for generics means that we can retrieve precise information on the type parameters used to instantiate a given object. Perhaps the biggest difference is that .NET provides a standard library, System.Reflection.Emit, to create PE assemblies and to populate them with CIL. The functionality of Reflection.Emit is roughly comparable to that of the BCEL and ASM tools mentioned in the previous subsection. Because it is part of the standard library, however, it is available to any program running on the CLI.
正如本节开头所建议的,脚本语言(以及 Lisp 和 Prolog)中的反射在某种意义上比 Java 或 C# 中的反射更“自然”:运行时需要详细的符号表信息来执行动态类型检查;在解释实现中,它也随时可用。Lisp 程序员几十年来都知道反射可用于许多其他目的。Java 和 C# 的设计者显然认为这些目的很有价值,足以证明将反射(实现复杂性相当高)添加到具有静态类型的编译语言中是合理的。
As suggested at the beginning of this section, reflection is in some sense more “natural” in scripting languages (and in Lisp and Prolog) than it is in Java or C#: detailed symbol table information is needed at run time to perform dynamic type checks; in an interpreted implementation, it is also readily available. Lisp programmers have known for decades that reflection was useful for many additional purposes. The designers of Java and C# clearly felt these purposes were valuable enough to justify adding reflection (with considerably higher implementation complexity) to a compiled language with static typing.
Java 和 C# 都允许程序员扩展编译器保存的元数据。在 Java 中,这些扩展采用附加在声明上的注解的形式。编程语言中内置了几种注解。它们起着编译指示的作用。例如,在示例 C-7.65 中,我们注意到,当将泛型类赋值给等效非泛型类的变量时,Java 编译器将生成警告。警告表示代码不是静态类型安全的,运行时可能会出现错误消息。如果程序员确信不会出现错误,则可以通过在出现赋值的方法前加上注解@SuppressWarnings(“unchecked”)来禁用编译时警告。
Both Java and C# allow the programmer to extend the metadata saved by the compiler. In Java, these extensions take the form of annotations attached to declarations. Several annotations are built into the programming language. These play the role of pragmas. In Example C-7.65, for example, we noted that the Java compiler will generate warnings when a generic class is assigned into a variable of the equivalent nongeneric class. The warning indicates that the code is not statically type-safe, and that an error message is possible at run time. If the programmer is certain that the error cannot arise, the compile-time warning can be disabled by prefixing the method in which the assignment appears with the annotation @SuppressWarnings(“unchecked”).
实际上,注释(属性)充当编译器支持的注释,具有明确定义的结构和 API,可让它们自动读取。正如我们所见,它们可以由编译器(作为指令)或反射程序读取。它们也可以由独立工具读取。这些工具的用途非常广泛。
In effect, annotations (attributes) serve as compiler-supported comments, with well-defined structure and an API that makes them accessible to automated perusal. As we have seen, they may be read by the compiler (as pragmas) or by reflective programs. They may also be read by independent tools. Such tools can be surprisingly versatile.
大多数程序员都熟悉符号调试器:它们内置于大多数编程语言解释器、虚拟机和集成程序开发环境中。它们也可以作为独立工具使用,其中最著名的可能是 GNU 的gdb。形容词符号是指调试器对高级语言语法(原始程序中的符号)的理解。早期的调试器只理解汇编语言。
Most programmers are familiar with symbolic debuggers: they are built into most programming language interpreters, virtual machines, and integrated program development environments. They are also available as stand-alone tools, of which the best known is probably GNU's gdb. The adjective symbolic refers to a debugger's understanding of high-level language syntax—the symbols in the original program. Early debuggers understood assembly language only.
在典型的调试会话中,用户在调试器的控制下启动程序,或将调试器附加到已经运行的程序。然后,调试器允许用户执行两种主要操作。一种检查或修改程序数据;另一种控制执行:启动、停止、单步执行以及建立断点和观察点。断点指定如果到达源代码中的特定位置则应停止执行。观察点指定如果读取或写入特定变量则应停止执行。断点和观察点通常都可以设为有条件的,这样只有当特定的布尔谓词计算为真时,执行才会停止。
In a typical debugging session, the user starts a program under the control of the debugger, or attaches the debugger to an already running program. The debugger then allows the user to perform two main kinds of operations. One kind inspects or modifies program data; the other controls execution: starting, stopping, stepping, and establishing breakpoints and watchpoints. A breakpoint specifies that execution should stop if it reaches a particular location in the source code. A watchpoint specifies that execution should stop if a particular variable is read or written. Both breakpoints and watchpoints can typically be made conditional, so that execution stops only if a particular Boolean predicate evaluates to true.
数据和控制操作都严重依赖于符号信息。符号调试器需要能够解析源语言表达式并将它们与原始程序中的符号关联起来。例如,在gdb中,命令 print ab[i]需要解析要打印的表达式;它还需要识别出a和i在程序当前停止点的范围内,并且b是一个数组类型字段,其索引范围包括i的当前值。类似地,命令break 123 if i+j == 3需要解析表达式i+j;它还需要识别出当前源文件的第 123 行有一个可执行语句,并且i和j在该行的范围内。
Both data and control operations depend critically on symbolic information. A symbolic debugger needs to be able both to parse source language expressions and to relate them to symbols in the original program. In gdb, for example, the command print a.b[i] needs to parse the to-be-printed expression; it also needs to recognize that a and i are in scope at the point where the program is currently stopped, and that b is an array-typed field whose index range includes the current value of i. Similarly, the command break 123 if i+j == 3 needs to parse the expression i+j; it also needs to recognize that there is an executable statement at line 123 in the current source file, and that i and j are in scope at that line.
数据和控制操作都依赖于从外部操纵程序的能力:停止和启动程序,以及读取和写入数据。这种控制至少可以通过三种方式实现。最简单的方式是在解释器中实现。由于解释器可以直接访问程序的符号表,并且“参与”每个语句的执行,因此在程序和调试器之间来回移动并让后者访问前者的数据是件很简单的事情。
Both data and control operations also depend on the ability to manipulate a program from outside: to stop and start it, and to read and write its data. This control can be implemented in at least three ways. The easiest occurs in interpreters. Since an interpreter has direct access to the program's symbol table and is “in the loop” for the execution of every statement, it is a straightforward matter to move back and forth between the program and the debugger, and to give the latter access to the former's data.
动态二进制重写技术(如 Dynamo 和 Pin)也可用于实现调试器控制 [ ZRA + 08 ]。然而,这项技术相对较新,在生产调试工具中尚未得到广泛应用。
The technology of dynamic binary rewriting (as in Dynamo and Pin) can also be used to implement debugger control [ZRA+08]. This technology is relatively new, however, and is not widely employed in production debugging tools.
对于编译程序,调试器控制的第三种实现是迄今为止最常见的。它依赖于操作系统的支持。在 Unix 中,它使用称为ptrace的内核服务。ptrace内核调用允许调试器“抓取”(附加到)现有进程或启动其控制下的进程。跟踪进程(调试器)可以拦截操作系统发送给被跟踪进程的任何信号,并可以读取和写入其寄存器和内存。如果被跟踪的进程当前正在运行,则调试器可以通过向其发送信号来停止它。如果当前已停止,则调试器可以指定应恢复执行的地址,并可以要求内核以单条指令(称为单步执行的进程)运行它或直到它收到另一个信号。
For compiled programs, the third implementation of debugger control is by far the most common. It depends on support from the operating system. In Unix, it employs a kernel service known as ptrace. The ptrace kernel call allows a debugger to “grab” (attach to) an existing process or to start a process under its control. The tracing process (the debugger) can intercept any signals sent to the traced process by the operating system and can read and write its registers and memory. If the traced process is currently running, the debugger can stop it by sending it a signal. If it is currently stopped, the debugger can specify the address at which it should resume execution, and can ask the kernel to run it for a single instruction (a process known as single stepping) or until it receives another signal.
不幸的是,跟踪进程和调试器之间反复切换上下文的开销会严重影响软件观察点的性能:速度减慢 1000 倍的情况并不罕见。基于动态二进制重写的调试器有可能支持任意数量的观察点,速度接近硬件观察点寄存器允许的速度。这个想法很简单:跟踪程序作为部分执行跟踪在调试器管理的跟踪缓存中运行。在生成每个跟踪时,调试器会在每次存储时以内联方式添加指令,以检查它是否写入x的地址,如果是,则跳回命令循环。
Unfortunately, the overhead of repeated context switches between the traced process and the debugger dramatically impacts the performance of software watchpoints: slowdowns of 1000× are not uncommon. Debuggers based on dynamic binary rewriting have the potential to support arbitrary numbers of watchpoints at speeds close to those admitted by hardware watchpoint registers. The idea is straightforward: the traced program runs as partial execution traces in a trace cache managed by the debugger. As it generates each trace, the debugger adds instructions at every store, in-line, to check whether it writes to x's address and, if so, to jump back to the command loop.
在将已调试的程序投入生产使用之前,人们通常希望了解(并尽可能提高)其性能。用于分析和剖析程序的工具既多又多种多样,以至于无法在此一一介绍。因此,我们将重点介绍本章中描述的运行时技术,这些技术在许多分析工具中占有突出地位。
Before placing a debugged program into production use, one often wants to understand—and if possible improve—its performance. Tools to profile and analyze programs are both numerous and varied—far too much so to even survey them here. We focus therefore on the run-time technologies, described in this chapter, that feature prominently in many analysis tools.
如果我们的程序由于算法原因而表现不佳,那么了解它大部分时间花在哪里可能就足够了。我们可以把注意力集中在在最重要的地方改进源代码。但是,如果程序由于其他原因而表现不佳,我们通常需要知道原因。可能是由于局部性差而导致缓存未命中?分支预测错误?处理器管道使用不当?解决这些问题和类似问题的工具通常依赖于更广泛的代码检测或某种硬件支持。
If our program is underperforming for algorithmic reasons, it maybe enough to know where it is spending the bulk of its time. We can focus our attention on improving the source code in the places it will matter most. If the program is underperforming for other reasons, however, we generally need to know why. Is it cache misses due to poor locality, perhaps? Branch mispredictions? Poor use of the processor pipeline? Tools to address these and similar questions generally rely on more extensive instrumentation of the code or on some sort of hardware support.
我们在本章开头将运行时系统定义为一组库,这些库对于许多语言实现至关重要,它们依赖于编译器或编译器生成的程序的知识。我们将这些库与“普通”库区分开来,普通库只需要传递的参数。
We began this chapter by defining a run-time system as the set of libraries, essential to many language implementations, that depend on knowledge of the compiler or the programs it produces. We distinguished these from “ordinary” libraries, which require only the arguments they are passed.
我们注意到,本书其他部分涵盖的几个主题,包括垃圾回收、可变长度参数列表、异常和事件处理、协同程序和线程、远程过程调用、事务内存和动态链接,通常被认为是运行时系统的权限。然后我们转向虚拟机,特别关注 Java 虚拟机 (JVM) 和公共语言基础结构 (CLI)。在机器代码后期绑定的一般标题下,我们考虑了即时和动态编译、二进制翻译和重写以及移动代码和沙盒。最后,在检查和自省的一般标题下,我们考虑了反射机制、符号调试和性能分析。
We noted that several topics covered elsewhere in the book, including garbage collection, variable-length argument lists, exception and event handling, coroutines and threads, remote procedure calls, transactional memory, and dynamic linking are often considered the purview of the run-time system. We then turned to virtual machines, focusing in particular on the Java Virtual Machine (JVM) and the Common Language Infrastructure (CLI). Under the general heading of late binding of machine code, we considered just-in-time and dynamic compilation, binary translation and rewriting, and mobile code and sandboxing. Finally, under the general heading of inspection and introspection, we considered reflection mechanisms, symbolic debugging, and performance analysis.
通过所有这些主题,我们看到了随着时间的推移复杂性的稳步增加。早期的 Basic 解释器一次解析和执行一个源语句。现代解释器首先将其源代码转换为语法树。早期的 Java 实现虽然仍然基于解释器,但依赖于单独的源代码到字节码编译器。现代 Java 实现以及 CLI 的实现都通过即时编译器提高了性能。对于在运行时扩展自身的程序,CLI 也允许动态调用源代码到字节码编译器,就像在 Common Lisp 中一样。最近的系统可能会对已经运行的程序进行分析和重新优化。类似的技术可能允许单独的工具从一种机器语言转换为另一种机器语言,或者对代码进行测试、调试、安全性、性能分析、模型检查或架构模拟。CLI 为跨语言互操作性提供了广泛的支持。
Through all these topics we have seen a steady increase in complexity over time. Early Basic interpreters parsed and executed one source statement at a time. Modern interpreters first translate their source into a syntax tree. Early Java implementations, while still interpreter-based, relied on a separate source-to-byte-code compiler. Modern Java implementations, as well as implementations of the CLI, enhance performance with a just-in-time compiler. For programs that extend themselves at run time, the CLI allows the source-to-byte-code compiler to be invoked dynamically as well, as it is in Common Lisp. Recent systems may profile and reoptimize already-running programs. Similar technology may allow separate tools to translate from one machine language to another, or to instrument code for testing, debugging, security, performance analysis, model checking, or architectural simulation. The CLI provides extensive support for cross-language interoperability.
许多这些发展已经模糊了编译器和运行时系统之间的界限,以及编译时和运行时操作之间的界限。可以肯定地说,这些趋势将继续下去。越来越多的人不再将程序视为静态产物,而是将其视为可塑组件的动态集合,具有丰富的语义结构,可以进行形式分析和重新配置。
Many of these developments have served to blur the line between the compiler and the run-time system, and between compile-time and run-time operations. It seems safe to predict that these trends will continue. More and more, programs will come to be seen not as static artifacts, but as dynamic collections of malleable components, with rich semantic structure amenable to formal analysis and reconfiguration.
16.1将 示例 15.4中的公式写成表达式树(一种语法树,其中每个运算符都由一个内部节点表示,该节点的子节点是其操作数)。通过合并相同的节点,将树转换为表达式DAG。评论树中的冗余以及它与图 15.4的关系。
16.1 Write the formula of Example 15.4 as an expression tree (a syntax tree in which each operator is represented by an internal node whose children are its operands). Convert your tree to an expression DAG by merging identical nodes. Comment on the redundancy in the tree and how it relates to Figure 15.4.
16.2在 示例 15.4和图 15.4中,我们假设a 、 b 、 c和s都是当前方法的前几个局部变量,可以用一条单字节指令将它们压入操作数栈或从操作数栈弹出。假设情况并非如此:即推送和弹出指令各需要三个字节。图 15.4左侧的代码现在需要多少字节?
大多数基于堆栈的语言(其中包括 Java 字节码和 CIL)都提供了交换指令(用于反转堆栈顶部两个值的顺序)和复制指令(用于推送当前位于堆栈顶部的值的第二个副本)。说明如何使用交换和复制来消除图 15.4左侧的弹出和推送s的操作。请随意利用乘法的结合性。您的新序列有多少条指令?多少字节?
16.2 We assumed in Example 15.4 and Figure 15.4 that a, b, c, and s were all among the first few local variables of the current method, and could be pushed onto or popped from the operand stack with a single one-byte instruction. Suppose that this is not the case: that is, that the push and pop instructions require three bytes each. How many bytes will now be required for the code on the left side of Figure 15.4?
Most stack-based languages, Java bytecode and CIL among them, provide a swap instruction that reverses the order of the top two values on the stack, and a duplicate instruction that pushes a second copy of the value currently at top of stack. Show how to use swap and duplicate to eliminate the pop and the pushes of s in the left side of Figure 15.4. Feel free to exploit the associativity of multiplication. How many instructions is your new sequence? How many bytes?
16.3 示例 16.5中的推测优化原则上可以静态执行。解释为什么动态编译器可以更有效地完成此操作。
16.3 The speculative optimization of Example 16.5 could in principle be statically performed. Explain why a dynamic compiler might be able to do it more effectively.
16.4 运行时插桩最常见的形式可能是计算每个基本块的执行次数。由于基本块很短,因此向每个块添加加载-增量-存储指令序列会对运行时间产生重大影响。
我们可以通过注意某些块意味着执行其他块来提高性能。例如,在if…then…else结构中,执行then部分或else部分意味着执行条件测试。如果我们聪明的话,我们实际上不必插桩测试。
描述一种通用技术,以尽量减少必须插桩的块数,以允许后处理器获得准确的计算每个块的计数。(这是一个难题。有关提示,请参阅 Larusand Ball 的论文 [ BL92 ]。)
16.4 Perhaps the most common form of run-time instrumentation counts the the number of times that each basic block is executed. Since basic blocks are short, adding a load-increment-store instruction sequence to each block can have a significant impact on run time.
We can improve performance by noting that certain blocks imply the execution of other blocks. In an if… then … else construct, for example, execution of either the then part or the else part implies execution of the conditional test. If we're smart, we won't actually have to instrument the test.
Describe a general technique to minimize the number of blocks that must be instrumented to allow a post-processor to obtain an accurate count for each block. (This is a difficult problem. For hints, see the paper byLarusand Ball [BL92].)
16.5 访问 software.intel.com/en-us/articles/pintool-downloads 并下载 Pin 的副本。使用它创建一个工具来分析循环。当给定一个(机器代码)程序及其输入时,该工具的输出应列出运行该程序时遇到的每个循环的次数。它还应为每个循环提供执行的迭代次数的直方图。
16.5 Visit software.intel.com/en-us/articles/pintool-downloads and download a copy of Pin. Use it to create a tool to profile loops. When given a (machine code) program and its input, the output of the tool should list the number of times that each loop was encountered when running the program. It should also give a histogram, for each loop, of the number of iterations executed.
16.6 概述二进制重写器可能使用的机制,无需访问源代码,即可捕获未初始化变量的使用、“双重删除”和已释放内存的使用(例如悬空指针)。在什么情况下,您可能能够捕获内存泄漏和越界数组访问?
16.6 Outline mechanisms that might be used by a binary rewriter, without access to source code, to catch uses of uninitialized variables, “double deletes,” and uses of deallocated memory (e.g., dangling pointers). Under what circumstances might you be able to catch memory leaks and out-of-bounds array accesses?
16.7扩展 图 16.4的代码以打印有关
16.7 Extend the code of Figure 16.4 to print information about
(b) 构造函数
(b) constructors
(三) 嵌套类
(c) nested classes
(d) 实现的接口
(d) implemented interfaces
(e) 祖先类及其方法、字段和构造函数
(e) ancestor classes, and their methods, fields, and constructors
(f) 方法抛出的异常
(f) exceptions thrown by methods
(七) 泛型类型参数
(g) generic type parameters
16.8 用 C# 重复上一个练习。添加有关参数名称和通用实例的信息。
16.8 Repeat the previous exercise in C#. Add information about parameter names and generic instances.
16.9 编写一个交互式工具,接受键盘命令来加载指定的类文件、创建类的实例、调用类的方法以及读写类的字段。可以将键盘输入限制为内置类型的值,并且只能在全局范围内工作。根据您的经验,评论为 Java 编写命令行解释器的可行性,类似于 Lisp、Prolog 或各种脚本语言常用的解释器。
16.9 Write an interactive tool that accepts keyboard commands to load specified class files, create instances of their classes, invoke their methods, and read and write their fields. Feel free to limit keyboard input to values of built-in types, and to work only in the global scope. Based on your experience, comment on the feasibility of writing a command-line interpreter for Java, similar to those commonly used for Lisp, Prolog, or the various scripting languages.
16.10在 Java 中,如果 p的具体类型是Foo,则 p.getClass()和Foo.class将返回相同的内容。请解释为什么在 Ruby、Python 或 JavaScript 中不能保证类似的等价性。有关提示,请参阅第 14.4.4 节。
16.10 In Java, if the concrete type of p is Foo, p.getClass() and Foo.class will return the same thing. Explain why a similar equivalence could not be guaranteed to hold in Ruby, Python, or JavaScript. For hints, see Section 14.4.4.
16.11 设计基于 Java 注释的“测试工具”系统。用户应该能够将注释附加到方法上,该注释指定要传递给方法测试运行的参数以及预期返回的值。为简单起见,您可以假设参数和返回值都是字符串或内置类型的实例。使用 Java 6 的注释处理功能,您应该在任何具有带有@Test注释的方法的类中自动生成一个新方法test()。此方法应使用指定的参数调用注释的方法,测试返回值并报告任何差异。它还应调用任何嵌套类的测试方法。确保包含一种机制来调用每个顶级类的测试方法。对于额外的挑战,除了返回的值之外,还要设计一种方法来指定单个方法的多个测试,以及一种测试抛出的异常的方法。
16.11 Design a “test harness” system based on Java annotations. The user should be able to attach to a method an annotation that specifies parameters to be passed to a test run of the method, and values expected to be returned. For simplicity, you may assume that parameters and return values will all be strings or instances of built-in types. Using the annotation processing facility of Java 6, you should automatically generate a new method, test() in any class that has methods with @Test annotations. This method should call the annotated methods with the specified parameters, test the return values, and report any discrepancies. It should also call the test methods of any nested classes. Be sure to include a mechanism to invoke the test method of every top-level class. For an extra challenge, devise a way to specify multiple tests of a single method, and a way to test exceptions thrown, in addition to values returned.
16.12 C++ 提供了一个typeid运算符,可用于查询指针或引用变量的具体类型:if (typeid(*p) == typeid(my_derived_type)) … typeid返回的值可以比较相等性,但不能赋值。它们还支持name()方法,该方法返回类型的(依赖于实现的)字符串名称。给出一个可以合理使用这些机制的程序片段示例。与更广泛的反射机制不同,typeid只能应用于具有至少一个虚方法的类(实例)。给出对这一限制的合理解释。
16.12 C++ provides a typeid operator that can be used to query the concrete type of a pointer or reference variable:
if (typeid(*p) == typeid(my_derived_type)) …
Values returned by typeid can be compared for equality but not assigned. They also support a name() method that returns an (implementation-dependent) character string name for the type. Give an example of a program fragment in which these mechanisms might reasonably be used.
Unlike more extensive reflection mechanisms, typeid can be applied only to (instances of) classes with at least one virtual method. Give a plausible explanation for this restriction.
16.13假设我们希望如 示例 16.36末尾所述,准确地将采样时间归因于调用子程序的各种上下文。也许最直接的方法是不仅记录当前 PC,还记录每次定时器中断时的堆栈回溯(动态链的内容)。不幸的是,这会大大增加分析开销。建议采用等效但更便宜的实现方式。
16.13 Suppose we wish, as described at the end of Example 16.36, to accurately attribute sampled time to the various contexts in which a subroutine is called. Perhaps the most straightforward approach would be to log not only the current PC but also the stack backtrace—the contents of the dynamic chain—on every timer interrupt. Unfortunately, this can dramatically increase profiling overhead. Suggest an equivalent but cheaper implementation.
16.14–16.17 更深入。
16.14–16.17 In More Depth.
16.18 了解 Java安全策略机制。程序员可以启用/禁止哪些方面的程序行为?这些策略是如何执行的?安全策略与边栏 16.3 中描述的验证过程之间的关系(如果有的话)是什么?
16.18 Learn about the Java security policy mechanism. What aspects of program behavior can the programmer enable/proscribe? How are such policies enforced? What is the relationship (if any) between security policies and the verification process described in Sidebar 16.3?
16.19 了解Perl 和 Ruby 中的污点模式。它与侧栏 16.5 中描述的通过二进制重写创建沙箱相比如何?它能捕获哪些类型的安全问题?它不能捕获哪些类型的问题?
16.19 Learn about taint mode in Perl and Ruby. How does it compare to sandbox creation via binary rewriting, as described in Sidebar 16.5? What sorts of security problems does it catch? What sorts of problems does it not catch?
16.20 了解带有证明的代码,这是一种技术,其中移动代码的供应商包含其安全性的证明,而用户只需验证该证明,而不是重新生成它(从 Necula [ Nec97 ] 的工作开始)。与其他形式的沙盒相比,这种技术如何?它可以保证哪些属性?
16.20 Learn about proof-carrying code, a technique in which the supplier of mobile code includes a proof of its safety, and the user simply verifies the proof, rather than regenerating it (start with the work of Necula [Nec97]). How does this technique compare to other forms of sandboxing? What properties can it guarantee?
16.21 研究Common Lisp 对象系统的基础MetaObject 协议(MOP)。它与 Java 和 C# 的反射机制相比如何?它能让你做哪些其他语言做不到的事情?
16.21 Investigate the MetaObject Protocol (MOP), which underlies the Common Lisp Object System. How does it compare to the reflection mechanisms of Java and C#? What does it allow you to do that these other languages do not?
16.22 使用符号调试器并设置断点来运行程序时,经常会发现“走得太远”,必须从头开始运行程序。这可能意味着为达到某一特定点而付出的所有努力都将付诸东流。考虑一下如何才能使程序不仅向前运行,而且向后运行。这种反向执行功能可能使用户能够缩小错误源的范围,就像缩小二分搜索的范围一样。考虑数据被覆盖时发生的信息丢失以及并行和事件驱动程序中出现的不确定性。
16.22 When using a symbolic debugger and moving through a program with breakpoints, one often discovers that one has gone “too far,” and must start the program over from the beginning. This may mean losing all the effort that had been put into reaching a particular point. Consider what it would take to be able to run the program not only forward but backward as well. Such a reverse execution facility might allow the user to narrow in on the source of bugs much as one narrows the range in binary search. Consider both the loss of information that happens when data is overridden and the nondeterminism that arises in parallel and event-driven programs.
16.23 下载并试用 Linux 中用于性能计数器采样的几个可用软件包之一(尝试 sourceforge.net/projects/perfctr/、perfmon2.sourceforge.net/ 或www.intel.com/software/pcm)。这些软件包允许您测量什么?您将如何使用这些信息?(注意:您可能需要安装内核补丁以使程序计数器可用于用户级代码。)
16.23 Download and experiment with one of the several available packages for performance counter sampling in Linux (try sourceforge.net/projects/perfctr/, perfmon2.sourceforge.net/, or www.intel.com/software/pcm). What do these packages allow you to measure? How might you use the information? (Note: you may need to install a kernel patch to make the program counters available to user-level code.)
16.24–16.25 更深入。
16.24–16.25 In More Depth.
Aycock [ Ayc03 ] 概述了即时编译的历史。有关 HotSpot 编译器和 JVM 的文档可在 Oracle 的网站上找到:www.oracle.com/technetwork/articles/javase/index-jsp-136373.html。JVM规范由 Lindholm 等人制定。[ LYBB14 ]。有关 CLI 的信息来源包括 ECMA 标准 [ Int12a、MR04 ] 和 msdn.microsoft.com 上的 .NET 页面。
Aycock [Ayc03] surveys the history of just-in-time compilation. Documentation on the HotSpot compiler and JVM can be found at Oracle's web site: www.oracle.com/technetwork/articles/javase/index-jsp-136373.html. The JVM specification is by Lindholm et al. [LYBB14]. Sources of information on the CLI include the ECMA standard [Int12a, MR04] and the .NET pages at msdn.microsoft.com.
Arnold 等人 [ AFG + 05 ] 对虚拟机上程序的自适应优化技术进行了广泛的调查。Deutsch 和 Schiffman [ DS84 ] 描述了 ParcPlace Smalltalk 虚拟机,该虚拟机开创了即时编译和缓存 JIT 编译的机器代码等机制。各种文章讨论了 Apple 的 68K 仿真器 [ Tho95 ]、DEC 的 FX!32 [ HH97b ] 及其早期的 VEST 和mx [ SCK + 93 ] 的二进制翻译技术。
Arnold et al. [AFG+05] provide an extensive survey of adaptive optimization techniques for programs on virtual machines. Deutsch and Schiffman [DS84] describe the ParcPlace Smalltalk virtual machine, which pioneered such mechanisms as just-in-time compilation and the caching of JIT-compiled machine code. Various articles discuss the binary translation technology of Apple's 68K emulator [Tho95], DEC's FX!32 [HH97b], and its earlier VEST and mx [SCK+93].
关于二进制重写的最佳信息来源可能是 Hazelwood 的文章 [ Haz11 ]。关于 Dynamo、Atom、Pin 和 QEMU 的原始论文分别由 Bala 等人 [ BDB00 ]、Srivastava 和 Eustace [ SE94 ]、Luk 等人 [ LCM + 05 ] 和 Bellard [ Bel05 ] 撰写。Duesterwald [ Due05 ] 借鉴了她在 Dynamo 项目中的经验,调查了动态二进制优化器设计中的问题。Wahbe 等人 [ WLAG93 ]报告了通过二进制重写进行沙盒处理的早期工作。
Probably the best source of information on binary rewriting is the text by Hazelwood [Haz11]. The original papers on Dynamo, Atom, Pin, and QEMU are by Bala et al. [BDB00], Srivastava and Eustace [SE94], Luk et al. [LCM+05], and Bellard [Bel05], respectively. Duesterwald [Due05] surveys issues in the design of a dynamic binary optimizer, drawing on her experience with the Dynamo project. Early work on sandboxing via binary rewriting is reported by Wahbe et al. [WLAG93].
DWARF 标准可从 dwarfstd.org [ DWA10 ] 获取;Eager 提供了更简单的介绍 [ Eag12 ]。Ball 和 Larus [ BL92 ] 描述了分析基本块执行所需的最少检测。Zhao 等人 [ ZRA + 08 ] 描述了如何使用动态检测(基于 DynamoRIO)来有效实现观察点。Martonosi 等人 [ MGA92 ] 描述了一种基于示例 16.37中概述的思想的性能分析工具。
The DWARF standard is available from dwarfstd.org [DWA10]; Eager provides a gentler introduction [Eag12]. Ball and Larus [BL92] describe the minimal instrumentation required to profile the execution of basic blocks. Zhao et al. [ZRA+08] describe the use of dynamic instrumentation (based on DynamoRIO) to implement watchpoints efficiently. Martonosi et al. [MGA92] describe a performance analysis tool that builds on the idea outlined in Example 16.37.
在 第 15 章 中,我们讨论了汇编和在编译器后端链接目标代码。我们介绍的技术导致代码正确但非常不理想:存在许多冗余计算,并且对现代微处理器的寄存器、多个功能单元和缓存的使用效率低下。本章将介绍代码改进:编译阶段致力于生成良好(快速)代码。如第 1.6.4 节所述,代码改进通常称为优化,尽管它很少使任何事情在绝对意义上达到最优。
In Chapter 15 we discussed the generation, assembly, and linking of target code in the back end of a compiler. The techniques we presented led to correct but highly suboptimal code: there were many redundant computations, and inefficient use of the registers, multiple functional units, and cache of a modern microprocessor. This chapter takes a look at code improvement: the phases of compilation devoted to generating good (fast) code. As noted in Section 1.6.4, code improvement is often referred to as optimization, though it seldom makes anything optimal in any absolute sense.
我们的研究将考虑简单的窥孔优化,它在非常小的指令窗口内“清理”生成的目标代码;局部优化,为各个基本块生成近乎最优的代码;以及全局优化,在整个子程序级别执行更积极的代码改进。我们不会介绍过程间改进;感兴趣的读者可以参考其他文本(参见本章末尾的参考书目注释)。此外,即使对于我们涵盖的主题,我们的意图也将更多地是“揭开”代码改进的神秘面纱,而不是详细描述该过程。大部分讨论将围绕单个子程序代码的连续改进展开。这个扩展示例将使我们能够说明几种关键形式的代码改进的效果,而无需详细讨论如何实现它们。
Our study will consider simple peephole optimization, which “cleans up” generated target code within a very small instruction window; local optimization, which generates near-optimal code for individual basic blocks; and global optimization, which performs more aggressive code improvement at the level of entire subroutines. We will not cover interprocedural improvement; interested readers are referred to other texts (see the Bibliographic Notes at the end of the chapter). Moreover, even for the subjects we cover, our intent will be more to “demystify” code improvement than to describe the process in detail. Much of the discussion will revolve around the successive refinement of code for a single subroutine. This extended example will allow us to illustrate the effect of several key forms of code improvement without dwelling on the details of how they are achieved.
更深入地
IN MORE DEPTH
第 17 章的完整内容可在配套网站上找到。
Chapter 17 can be found in its entirety on the companion site.
本附录提供了本书中提到的每种主要编程语言的简要说明、参考书目和(在许多情况下)在线信息的 URL。截至 2015 年 6 月,这些 URL 都是准确的,但随着人们移动文件,它们可能会发生变化。在参考书目中可以找到一些其他 URL。
This appendix provides brief descriptions, bibliographic references, and (in many cases) URLs for on-line information concerning each of the principal programming languages mentioned in this book. The URLs are accurate as of June 2015, though they are subject to change as people move files around. Some additional URLs can be found in the bibliographic references.
Bill Kinnersley 在people.ku.edu/~nkinners/LangList/Extras/langlist.htm上维护了大约 2500 种编程语言的在线资料索引。
Bill Kinnersley maintains an index of on-line materials for approximately 2500 programming languages at people.ku.edu/~nkinners/LangList/Extras/langlist.htm.
图 A.1显示了一些较有影响力或广泛使用的编程语言的谱系。每种语言的日期表示其特性广为人知的大致时间。箭头表示对设计的主要影响。当然,许多影响无法在一张图中显示出来。
Figure A.1 shows the genealogy of some of the more influential or widely used programming languages. The date for each language indicates the approximate time at which its features became widely known. Arrows indicate principal influences on design. Many influences, of course, cannot be shown in a single figure.
Ada 最初旨在成为美国国防部 [ Ame83 ] 委托开发的所有软件的标准语言,现在由 ISO [ Int12b ] 标准化。原型由多个站点的团队设计;最终的 '83 语言由位于明尼阿波利斯的 Honeywell 系统和研究中心和法国的 Alsys 公司的一个团队在 Jean Ichbiah 的领导下开发。一种非常庞大的语言,主要源自 Pascal。设计原理在一份非常清晰的配套文档 [ IBFW91 ] 中阐明。Ada 95 是 Intermetrics, Inc. 的一个团队根据政府合同开发的修订版。它修复了早期语言中的几个细微问题,并添加了对象、共享内存同步和许多其他功能。Ada 2005 和 Ada 2012 添加了许多附加功能;有关摘要,请参阅ada2012.org/comparison.html。免费提供的实现(gnat)作为 GNU 编译器集合(gcc)的一部分分发。更多资源请访问adaic.org/和ada-europe.org/。
Ada Originally intended to be the standard language for all software commissioned by the U.S. Department of Defense [Ame83], now standardized by the ISO [Int12b]. Prototypes designed by teams at several sites; final '83 language developed by a team at Honeywell's Systems and Research Center in Minneapolis and Alsys Corp. in France, led by Jean Ichbiah. A very large language, descended largely from Pascal. Design rationale articulated in a remarkably clear companion document [IBFW91]. Ada 95 was a revision developed under government contract by a team at Intermetrics, Inc. It fixed several subtle problems in the earlier language, and added objects, shared-memory synchronization, and many other features. Ada 2005 and Ada 2012 add a host of additional features; for a summary see ada2012.org/comparison.html. Freely available implementation (gnat) distributed as part of the GNU compiler collection (gcc). Additional resources at adaic.org/ and ada-europe.org/.
Algol 60 最初的块结构语言。Naur 等人的定义 [ NBB + 63 ] 被认为是清晰简洁的里程碑。它包括巴科斯范式 (BNF) 的原始使用。
Algol 60 The original block-structured language. The definition by Naur et al. [NBB+63] is considered a landmark of clarity and conciseness. It includes the original use of Backus-Naur Form (BNF).
Algol 68 Algol 60 的大型且相对复杂的后继者,由 A. van Wijngaarden 领导的委员会设计。包括(除其他外)结构和联合、基于表达式的语法、引用参数、变量的引用模型和并发性。官方定义 [ vMP + 75 ] 使用使用非常规术语,阅读起来非常困难;其他来源(例如,Pagan 的书 [ Pag76 ])更容易理解。
Algol 68 A large and relatively complex successor to Algol 60, designed by a committee led by A. van Wijngaarden. Includes (among other things) structures and unions, expression-based syntax, reference parameters, a reference model of variables, and concurrency. The official definition [vMP+75] uses unconventional terminology and is very difficult to read; other sources (e.g., Pagan's book [Pag76]) are more accessible.
Algol W Niklaus Wirth 和 CAR Hoare [ WH66、 Sit72 ]提出的 Algol 68 的更小、更简单的替代方案。Pascal 的前身。引入了case语句。
Algol W A smaller, simpler alternative to Algol 68, proposed by Niklaus Wirth and C. A. R. Hoare [WH66, Sit72]. The precursor to Pascal. Introduced the case statement.
APL 由 Kenneth Iverson 在 20 世纪 50 年代末和 60 年代初设计,主要用于操作数值数组。功能强大。极其简洁。运算符集强大。使用扩展字符集。旨在用于交互。原始语法 [ Ive62 ] 是非线性的;由于 IBM 团队的改进,实现通常使用修订的语法 [ IBM87 ]。在线资源位于sigapl.org/。
APL Designed by Kenneth Iverson in the late 1950s and early 1960s, primarily for the manipulation of numeric arrays. Functional. Extremely concise. Powerful set of operators. Employs an extended character set. Intended for interactive use. Original syntax [Ive62] was nonlinear; implementations generally use a revised syntax due to a team at IBM [IBM87]. On-line resources at sigapl.org/.
Basic 简单的命令式语言,最初用于交互用途。原始版本由达特茅斯学院的 John Kemeny 和 Thomas Kurtz 于 20 世纪 60 年代初开发。存在数十种方言。Microsoft 的 Visual Basic 与原始版本几乎没有相似之处,是当今使用最广泛的语言(资源可在msdn.microsoft.com/en-us/library/sh9ywfdk.aspx获得)。ANSI 标准 [ Ame78 ]定义的最小子集。
Basic Simple imperative language, originally intended for interactive use. Original version developed by John Kemeny and Thomas Kurtz of Dartmouth College in the early 1960s. Dozens of dialects exist. Microsoft's Visual Basic, which bears little resemblance to the original, is the most widely used today (resources available at msdn.microsoft.com/en-us/library/sh9ywfdk.aspx). Minimal subset defined by ANSI standard [Ame78].
C 最成功的命令式语言之一。最初由贝尔实验室的 Brian Kernighan 和 Dennis Ritchie 在开发 Unix [ KR88 ] 过程中定义。语法简洁。声明语法不常见。旨在用于系统编程。弱类型检查。无动态语义检查。1990 年由 ANSI/ISO 标准化 [ Ame90 ]。1994 年采用国际字符集扩展。1999 年和 2011 年采用了更广泛的更改 [ Int99、 Int11 ]。流行的开源实现包括gcc ( gnu.org/software/gcc/ ) 和clang/llvm ( clang.llvm.org/ )。
C One of the most successful imperative languages. Originally defined by Brian Kernighan and Dennis Ritchie of Bell Labs as part of the development of Unix [KR88]. Concise syntax. Unusual declaration syntax. Intended for systems programming. Weak type checking. No dynamic semantic checks. Standardized by ANSI/ISO in 1990 [Ame90]. Extensions for international character sets adopted in 1994. More extensive changes adopted in 1999 and 2011 [Int99, Int11]. Popular open-source implementations include gcc (gnu.org/software/gcc/) and clang/llvm (clang.llvm.org/).
C# 面向对象语言,最初由 Anders Hejlsberg、Scott Wiltamuth 和 Microsoft Corporation 的同事在 20 世纪 90 年代末和 21 世纪初设计 [ HTWG11、 Mic12、 ECM06a ]。版本 1 和 2 由 ECMA 和 ISO [ ECM06a ] 标准化;后续版本由 Microsoft 直接定义。用作 .NET 平台的主要语言,该平台是用于多语言分布式计算的运行时和中间件系统。包括 Java 的大部分功能以及 C++ 和 Visual Basic 的许多功能,包括引用和值类型、连续和行指针数组、虚拟和非虚拟方法、运算符重载、委托、泛型、局部类型推断、带有指针的“不安全”超集以及跨语言边界调用动态类型对象的方法的能力。商业资源位于msdn.microsoft.com/en-us/vstudio/hh388566.aspx。可在mono-project.com/docs/about-mono/languages/csharp/免费获取实现
C# Object-oriented language originally designed by Anders Hejlsberg, Scott Wiltamuth, and associates at Microsoft Corporation in the late 1990s and early 2000s [HTWG11, Mic12, ECM06a]. Versions 1 and 2 standardized by ECMA and the ISO [ECM06a]; subsequent versions defined directly by Microsoft. Serves as the principal language for the .NET platform, a runtime and middleware system for multilanguage distributed computing. Includes most of Java's features, plus many from C++ and Visual Basic, including both reference and value types, both contiguous and row-pointer arrays, both virtual and nonvirtual methods, operator overloading, delegates, generics, local type inference, an “unsafe” superset with pointers, and the ability to invoke methods of dynamically typed objects across language boundaries. Commercial resources at msdn.microsoft.com/en-us/vstudio/hh388566.aspx. Freely available implementation at mono-project.com/docs/about-mono/languages/csharp/.
C++ 第一个得到广泛采用的面向对象 C 语言后继者。至今仍被广泛认为是最适合“工业强度”计算的语言。最初由贝尔实验室(现位于德克萨斯农工大学)的 Bjarne Stroustrup 设计。一种大型语言。1998 年由 ISO 标准化;2011 年进行了重大修订;2014 年进行了更新。包括(除其他外)通用引用类型、静态和动态方法绑定、广泛的功能用于重载和强制、多重继承和图灵完备泛型。没有自动垃圾收集。有许多文本;除了 ISO 标准 [ Int14b ] 之外,Stroustrup 的文本是最全面的 [ Str13 ]。gcc和clang/llvm发行版中包含免费提供的实现(参见 C)。Stroustrup 自己的资源页面位于stroustrup.com/C++.html。
C++ The first object-oriented successor to C to gain widespread adoption. Still widely considered the one most suited to “industrial strength” computing. Originally designed by Bjarne Stroustrup of Bell Labs (now at Texas A&M University). A large language. Standardized by the ISO in 1998; major revision in 2011; updated in 2014. Includes (among other things) generalized reference types, both static and dynamic method binding, extensive facilities for overloading and coercion, multiple inheritance, and Turing-complete generics. No automatic garbage collection. Many texts exist; aside from the ISO standard [Int14b], Stroustrup's is the most comprehensive [Str13]. Freely available implementations included in the gcc and clang/llvm distributions (see C). Stroustrup's own resource page is at stroustrup.com/C++.html.
Caml 和 OCaml Caml 是 ML 的一种方言,由 Guy Cousineau 及其同事在 INRIA(法国国家研究机构)于 20 世纪 80 年代末开发。在 Xavier Leroy 的领导下,该语言于 1996 年左右演变为 Objective Caml (OCaml);修订后的语言添加了模块和面向对象。许多人认为 OCaml 比 SML(另一种主要的 ML 方言)“更实用”,因此在工业界得到广泛应用。另请参阅 F#。在线资源请访问caml.inria.fr/。
Caml and OCaml Caml is a dialect of ML developed by Guy Cousineau and colleagues at INRIA (the French National research institute) beginning in the late 1980s. Evolved into Objective Caml (OCaml) around 1996, under the leadership of Xavier Leroy; the revised language adds modules and object orientation. Regarded by many as “more practical” than SML (the other principal ML dialect), OCaml is widely used in industry. See also F#. On-line resources at caml.inria.fr/.
雪松 参见台地和雪松。
Cedar See Mesa and Cedar.
Cilk 麻省理工学院的 Charles Leiserson 及其同事于 20 世纪 90 年代中期开始开发 C 和 C++ 的并发扩展,并于 2006 年由 Cilk Arts, Inc. 商业化;2009 年被英特尔收购。C 的扩展刻意被最小化:函数可以作为单独的任务生成;可以使用sync等待子任务的完成;任务可以在有限的程度上通过intake相互同步。实现采用一种新颖、可证明有效的工作窃取调度程序。在线资源位于supertech.csail.mit.edu/cilk/和/software.intel.com/en-us/intel-cilk-plus。
Cilk Concurrent extension of C and C++ developed by Charles Leiserson and associates at MIT beginning in the mid 1990s, and commercialized by Cilk Arts, Inc. in 2006; acquired by Intel in 2009. Extensions to C are deliberately minimal: a function can be spawned as a separate task; completion of subtasks can be awaited en masse with sync; tasks can synchronize with each other to a limited degree with inlets. Implementation employs a novel, provably efficient work-stealing scheduler. On-line resources at supertech.csail.mit.edu/cilk/ and /software.intel.com/en-us/intel-cilk-plus.
CLOS Common Lisp 对象系统 [ Kee89; Sei05,第 16-17 章]。一组面向对象的 Common Lisp 扩展,已纳入 ANSI 标准语言(请参阅 Common Lisp)。
CLOS The Common Lisp Object System [Kee89; Sei05, Chaps. 16–17]. A set of object-oriented extensions to Common Lisp, incorporated into the ANSI standard language (see Common Lisp).
Clu 由 Barbara Liskov 及其同事于 20 世纪 70 年代末在麻省理工学院开发 [ LG86 ]。旨在提供一组异常强大的数据抽象功能 [ LSAS77 ]。还包括迭代器和异常处理。文档和免费提供的实现位于pmg.csail.mit.edu/CLU.html。
Clu Developed by Barbara Liskov and associates at MIT in the late 1970s [LG86]. Designed to provide an unusually powerful set of features for data abstraction [LSAS77]. Also includes iterators and exception handling. Documentation and freely available implementations at pmg.csail.mit.edu/CLU.html.
Cobol 最初由美国国防部于 20 世纪 50 年代末和 60 年代初由 Grace Murray Hopper 领导的团队开发。长期以来,它是世界上使用最广泛的编程语言。1968 年由 ANSI 标准化;1974 年和 1985 年修订。主要用于业务数据处理。引入了结构概念。精心设计的 I/O 设施。Cobol 2002 和 2014 [ Int14a ] 添加了各种现代语言功能,包括面向对象。
Cobol Originally developed by the U.S. Department of Defense in the late 1950s and early 1960s by a team led by Grace Murray Hopper. Long the most widely used programming language in the world. Standardized by ANSI in 1968; revised in 1974 and 1985. Intended principally for business data processing. Introduced the concept of structures. Elaborate I/O facilities. Cobol 2002 and 2014 [Int14a] add a variety of modern language features, including object orientation.
Common Lisp 大型、广泛使用的 Lisp 方言(另请参阅 Lisp)。包括(除其他外)静态作用域、广泛的类型系统、异常处理和面向对象功能(请参阅 CLOS)。多年来,标准参考是 Guy Steele, Jr. 的书 [ Ste90 ]。随后由 ANSI [ Ame96b ]标准化;Seibel [ Sei05 ]的最新文本,标准的精简超文本版本可在lispworks.com/documentation/HyperSpec/Front/index.htm 上找到。
Common Lisp Large, widely used dialect of Lisp (see also Lisp). Includes (among other things) static scoping, an extensive type system, exception handling, and object-oriented features (see CLOS). For years the standard reference was the book by Guy Steele, Jr. [Ste90]. Subsequently standardized by ANSI [Ame96b]; recent text by Seibel [Sei05] Abridged hypertext version of the standard available at lispworks.com/documentation/HyperSpec/Front/index.htm.
CSP 参见奥卡姆。
CSP See Occam.
埃菲尔 一种面向对象语言,由 Bertrand Meyer 及其巴黎逻辑工具学会的同事开发 [ Mey92b,ECM06b ]。包括(除其他功能外)多重继承、自动垃圾收集和强大的机制,用于重命名派生类中的数据成员和方法。在线资源位于eiffel.com/。
Eiffel An object-oriented language developed by Bertrand Meyer and associates at the Société des Outils du Logiciel à Paris [Mey92b, ECM06b]. Includes (among other things) multiple inheritance, automatic garbage collection, and powerful mechanisms for renaming of data members and methods in derived classes. On-line resources at eiffel.com/.
Erlang 一种函数式语言,广泛支持分布式、容错和消息传递。由 Joe Armstrong 及其同事于 20 世纪 80 年代末在爱立信计算机科学实验室开发 [ Arm13 ]。自 1998 年起作为开源软件发布。用于实现爱立信和其他公司的各种产品,尤其是欧洲电信行业。在线资源请访问erlang.org/。
Erlang A functional language with extensive support for distribution, fault tolerance, and message passing. Developed by Joe Armstrong and colleagues at Ericsson Computer Science Laboratory starting in the late 1980s [Arm13]. Distributed as open source since 1998. Used to implement a variety of products from Ericsson and other companies, particularly in the European telecom industry. On-line resources at erlang.org/.
Euclid 命令式语言,由 Butler Lampson 及其同事于 20 世纪 70 年代中期在施乐帕洛阿尔托研究中心开发 [ LHL + 77 ]。旨在消除 Pascal 中许多常见编程错误的来源,并促进程序的形式化验证。具有封闭的作用域和模块类型。
Euclid Imperative language developed by Butler Lampson and associates at the Xerox Palo Alto Research Center in the mid-1970s [LHL+77]. Designed to eliminate many of the sources of common programming errors in Pascal, and to facilitate formal verification of programs. Has closed scopes and module types.
F# 微软研究院的 Don Syme 及其同事开发的 OCaml 的后代。首次公开发布于 2005 年 [ SGC13 ]。与 OCaml 的区别主要在于它能够与 .NET 框架集成。在线资源位于research.microsoft.com/fsharp/和msdn.microsoft.com/en-us/library/dd233154.aspx。
F# A descendant of OCaml developed by Don Syme and colleagues at Microsoft Research. First public release was in 2005 [SGC13]. Differences from OCaml are primarily to accommodate integration with the .NET framework. On-line resources at research.microsoft.com/fsharp/ and msdn.microsoft.com/en-us/library/dd233154.aspx.
Forth 一门小型而精巧的基于堆栈的语言,专为资源有限的机器解释而设计 [ Bro87, Int97 ]。最初由 Charles H. Moore 于 20 世纪 60 年代末开发。在仪器仪表和过程控制社区拥有一批忠实的追随者。
Forth A small and rather ingenious stack-based language designed for interpretation on machines with limited resources [Bro87, Int97]. Originally developed by Charles H. Moore in the late 1960s. Has a loyal following in the instrumentation and process-control communities.
Fortran 最初的高级命令式语言。由 IBM 的 John Backus 及其同事于 20 世纪 50 年代中期开发。重要的历史版本包括 Fortran I、Fortran II、Fortran IV、Fortran 77 和 Fortran 90。后两个版本记录在一对 ANSI 标准中。Fortran 90 [ MR96 ](1995 年更新)是对该语言的一次重大修订,添加了(除其他内容外)递归、指针、新控制结构和大量数组操作。Fortran 2003 添加了面向对象。Fortran 2008 [ Int10 ](2010 年批准)添加了几个泛型、联合数组(用于分布式内存计算机的具有明确额外“位置”维度的数组)以及用于迭代独立的循环的DO CONCURRENT结构。Fortran 77 继续被广泛使用。免费提供的gfortran实现符合所有现代标准,并作为gcc编译器套件 ( gnu.org/software/gcc/fortran/ ) 的一部分进行分发。从gcc版本 3.4开始,不再支持较旧的g77前端。
Fortran The original high-level imperative language. Developed in the mid-1950s by John Backus and associates at IBM. Important historical versions include Fortran I, Fortran II, Fortran IV, Fortran 77, and Fortran 90. The latter two were documented in a pair of ANSI standards. Fortran 90 [MR96] (updated in 1995) was a major revision to the language, adding (among other things) recursion, pointers, new control constructs, and a wealth of array operations. Fortran 2003 adds object orientation. Fortran 2008 [Int10] (approved in 2010) adds several generics, co-arrays (arrays with an explicit extra “location” dimension for distributed-memory machines), and a DO CONCURRENT construct for loops whose iterations are independent. Fortran 77 continues to be widely used. Freely available gfortran implementation conforms to all modern standards, and is distributed as part of the gcc compiler suite (gnu.org/software/gcc/fortran/). Support for the older g77 front end was discontinued as of gcc version 3.4.
Go 是 一种静态类型语言,由 Google 的 Robert Griesemer、Rob Pike、Ken Thompson 及其同事于 2007 年开始开发。旨在将脚本语言的简单性与编译的效率结合起来,部分原因是人们认为 C++ 已经变得过于庞大和复杂。包括垃圾收集、强类型、本地类型推断、类型扩展和接口(作为类的替代)、可变长度和关联数组以及基于消息的并发。在 Google 内部和外部均积极使用。在线资源位于golang.org/。
Go A statically typed language developed by Robert Griesemer, Rob Pike, Ken Thompson, and colleagues at Google, beginning in 2007. Intended to combine the simplicity of scripting languages with the efficiency of compilation, and motivated in part by the perception that C++ had grown too large and complicated. Includes garbage collection, strong typing, local type inference, type extension and interfaces as an alternative to classes, variable-length and associative arrays, and message-based concurrency. In active use both within and outside Google. Online resources at golang.org/.
Haskell 领先的纯函数式语言。源自 Miranda。由一个研究委员会于 1987 年开始设计。包括柯里化函数、高阶函数、非严格语义、静态多态类型、模式匹配、列表推导、模块、单子 I/O、类型类和基于布局(缩进)的语法分组。Haskell 98 多年来一直是标准;已被 Haskell 2010 [ PJ10 ] 取代。在线资源位于haskell.org/。还设计了几种并发变体,包括 Concurrent Haskell [ JGF96 ] 和 pH [ NA01 ]。
Haskell The leading purely functional language. Descended from Miranda. Designed by a committee of researchers beginning in 1987. Includes curried functions, higher order functions, nonstrict semantics, static polymorphic typing, pattern matching, list comprehensions, modules, monadic I/O, type classes, and layout (indentation)-based syntactic grouping. Haskell 98 was for many years the standard; superceded by Haskell 2010 [PJ10]. On-line resources at haskell.org/. Several concurrent variants have also been devised, including Concurrent Haskell [JGF96] and pH [NA01].
Icon Snobol 的后继者。由亚利桑那大学的 Ralph Griswold(Snobol 的首席设计师)开发 [ GG96 ]。采用更传统的控制流结构,但具有基于模式匹配和回溯的强大迭代和搜索功能。在线资源位于cs.arizona.edu/icon/。
Icon The successor to Snobol. Developed by Ralph Griswold (Snobol's principal designer) at the University of Arizona [GG96]. Adopts more conventional control-flow constructs, but with powerful iteration and search facilities based on pattern matching and backtracking. On-line resources at cs.arizona.edu/icon/.
Java 一种面向对象语言,主要基于 C++ 的一个子集。由 Sun Microsystems 的 James Gosling 及其同事于 20 世纪 90 年代初开发 [ AG06、 GJS + 14 ]。旨在构建高度可移植、与体系结构无关的程序。与用于在 Java虚拟机上执行的中间字节码格式一起定义[ LYBB14 ]。包括(除其他内容外)变量(类类型)参考模型、混合继承、线程以及用于图形、通信和其他活动的大量预定义库。在线资源位于docs.oracle.com/javase/。
Java Object-oriented language based largely on a subset of C++. Developed by James Gosling and associates at Sun Microsystems in the early 1990s [AG06, GJS+14]. Intended for the construction of highly portable, architecture-neutral programs. Defined in conjunction with an intermediate bytecode format intended for execution on a Java virtual machine [LYBB14]. Includes (among other things) a reference model of (class-typed) variables, mix-in inheritance, threads, and extensive predefined libraries for graphics, communication, and other activities. On-line resources at docs.oracle.com/javase/.
JavaScript 由 Netscape Corp. 的 Brendan Eich 于 20 世纪 90 年代中期开发的简单脚本语言,用于客户端 Web 脚本编写。除了表面上的语法相似性之外,与 Java 没有任何联系。嵌入在大多数商业 Web 浏览器中。Microsoft 的 JScript 非常相似。两者于 1997 年合并为单一 ECMA 标准;随后由 ISO [ ECM11 ] 修订并交叉标准化。
JavaScript Simple scripting language developed by Brendan Eich at Netscape Corp. in the mid 1990s for the purpose of client-side web scripting. Has no connection to Java beyond superficial syntactic similarity. Embedded in most commercial web browsers. Microsoft's JScript is very similar. The two were merged into a single ECMA standard in 1997; subsequently revised and cross-standardized by the ISO [ECM11].
Lisp 最初的函数式语言 [ McC60 ]。由 John McCarthy 于 20 世纪 50 年代末开发,是 Church 的 lambda 演算的实现。存在许多方言。目前最常见的两种是 Common Lisp 和 Scheme(参见单独的条目)。历史上重要的方言包括 Lisp 1.5 [ MAE + 65 ]、MacLisp [ Moo78 ] 和 Interlisp [ TM81 ]。
Lisp The original functional language [McC60]. Developed by John McCarthy in the late 1950s as a realization of Church's lambda calculus. Many dialects exist. The two most common today are Common Lisp and Scheme (see separate entries). Historically important dialects include Lisp 1.5 [MAE+65], MacLisp [Moo78], and Interlisp [TM81].
Lua 轻量级脚本语言,主要用于扩展/嵌入设置。最初由里约热内卢天主教大学的 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo 开发。旨在简单、快速、易于扩展和移植到新环境。标准实现也相当小——解释器和所有标准库都远低于 1 MB。在游戏行业以及其他各种领域。在线资源请访问lua.org/。
Lua Lightweight scripting language intended primarily for extension/embedded settings. Originally developed by Roberto Ierusalimschy, Waldemar Celes, and Luiz Henrique de Figueiredo at the Pontifical Catholic University of Rio de Janeiro. Intended to be simple, fast, and easy to extend and to port to new environments. The standard implementation is also quite small—well under 1 MB for the interpreter and all the standard libraries. Heavily used in the gaming industry and in a wide variety of other fields. Online resources at lua.org/.
Mesa 和 Cedar Mesa [ LR80 ] 是 Euclid 的后继者,由 Butler Lampson 领导的团队于 20 世纪 70 年代在施乐公司的帕洛阿尔托研究中心开发。包括基于监视器的并发性。与 Interlisp 和 Smalltalk 一起,是率先使用个人工作站的三个同伴项目之一,具有位图显示、鼠标和图形用户界面。Cedar [ SZBH86 ] 是 Mesa 的后继者,具有(除其他外)完整的类型安全、异常和自动垃圾收集功能。
Mesa and Cedar Mesa [LR80] was a successor to Euclid developed in the 1970s at Xerox's Palo Alto Research Center by a team led by Butler Lampson. Includes monitor-based concurrency. Along with Interlisp and Smalltalk, one of three companion projects that pioneered the use of personal workstations, with bitmapped displays, mice, and a graphical user interface. Cedar [SZBH86] was a successor to Mesa with (among other things) complete type safety, exceptions, and automatic garbage collection.
Miranda 由 David Turner 在 20 世纪 80 年代中期设计的纯函数式语言 [ Tur86 ]。源自 ML;具有类型推断和自动柯里化功能。添加了列表推导 (第 8.6 节),并对所有参数使用惰性求值。使用缩进和换行符进行语法分组。在线资源位于miranda.org.uk/。
Miranda Purely functional language designed by David Turner in the mid-1980s [Tur86]. Descended from ML; has type inference and automatic currying. Adds list comprehensions (Section 8.6), and uses lazy evaluation for all arguments. Uses indentation and line breaks for syntactic grouping. On-line resources at miranda.org.uk/.
ML 具有“类似 Pascal”语法的函数式语言。最初由爱丁堡大学的 Robin Milner 及其同事于 20 世纪 70 年代中后期设计,作为程序验证系统的元语言(因此得名)。率先实现了积极的编译时类型推断和多态性。具有一些必要的特性。存在几种方言;最广泛使用的有标准 ML [ MTHM97 ] 和 OCaml(参见单独条目)。新泽西标准 ML 是普林斯顿大学和贝尔实验室的一个项目,已为许多平台生成了免费可用的实现( smlnj.org/)。
ML Functional language with “Pascal-like” syntax. Originally designed in the mid- to late 1970s by Robin Milner and associates at the University of Edinburgh as the meta-language (hence the name) for a program verification system. Pioneered aggressive compile-time type inference and polymorphism. Has a few imperative features. Several dialects exist; the most widely used are Standard ML [MTHM97] and OCaml (see separate entry). Standard ML of New Jersey, a project of Princeton University and Bell Labs, has produced freely available implementations for many platforms (smlnj.org/).
Modula 和 Modula-2 是 Pascal 的直接后继者,由 Niklaus Wirth 开发。最初的 Modula [ Wir77b ] 是一种明确基于并发监视器的语言。有时它也被称为 Modula (1),以区别于它的后继者。更有影响力的 Modula-2 [ Wir85b ] 最初设计时就加入了协程(第 9.5 节),但没有真正的并发功能。这两种语言都提供了模块管理器式数据抽象机制。Modula-2 于 1996 年由 ISO 标准化 [ Int96 ]。可从nongnu.org/gm2/获取适用于多种平台的免费实现。
Modula and Modula-2 The immediate successors to Pascal, developed by Niklaus Wirth. The original Modula [Wir77b] was an explicitly concurrent monitor-based language. It is sometimes called Modula (1) to distinguish it from its successors. The more influential Modula-2 [Wir85b] was originally designed with coroutines (Section 9.5), but no real concurrency. Both languages provide mechanisms for module-as-manager style data abstractions. Modula-2 was standardized by the ISO in 1996 [Int96]. Freely available implementation for several platforms available from nongnu.org/gm2/.
Modula-3 是 Modula-2 的一个主要扩展,由 Luca Cardelli、Jim Donahue、Mick Jordan、Bill Kalsow 和 Greg Nelson 在 20 世纪 80 年代末在数字系统研究中心和 Olivetti 研究中心开发 [ Har92 ]。旨在提供与 Ada 相当的对大型、可靠和可维护系统的支持,但形式更简单、更优雅。在线资源请访问modula3.org/。
Modula-3 A major extension to Modula-2 developed by Luca Cardelli, Jim Donahue, Mick Jordan, Bill Kalsow, and Greg Nelson at the Digital Systems Research Center and the Olivetti Research Center in the late 1980s [Har92]. Intended to provide a level of support for large, reliable, and maintainable systems comparable to that of Ada, but in a simpler and more elegant form. On-line resources at modula3.org/.
Oberon Niklaus Wirth 设计的一种刻意简化的语言 [ Wir88b , RW92 ]。本质上是 Modula-2 [ Wir88a ]的子集,增加了类型扩展机制 (第 10.2.4 节) [ Wir88c ]。在线资源请访问oberon.ethz.ch/。
Oberon A deliberately minimal language designed by Niklaus Wirth [Wir88b, RW92]. Essentially a subset of Modula-2 [Wir88a], augmented with a mechanism for type extension (Section 10.2.4) [Wir88c]. On-line resources at oberon.ethz.ch/.
Objective-C 基于 Smalltalk 风格“消息传递”的面向对象 C 语言扩展。由 Brad Cox 和 StepStone 公司于 20 世纪 80 年代初设计。NeXT Software, Inc. 于 20 世纪 80 年代末采用该语言,用于其NeXTStep 操作系统和编程环境。Apple 于 1997 年收购 NeXT 后,将其作为 Mac OS X 的主要开发语言。比其他面向对象的 C 语言后代简单得多。以完全动态的方法调度和不寻常的消息语法为特色。gcc发行版中包含免费提供的实现(参见 C)。在线文档可通过 developer.apple.com/library/mac/navigation 搜索找到。Apple未来的开发将转向 Swift(参见单独条目)。
Objective-C An object-oriented extension to C based on Smalltalk-style “messaging.” Designed by Brad Cox and StepStone corporation in the early 1980s. Adopted by NeXT Software, Inc., in the late 1980s for their NeXTStep operating system and programming environment. Adopted by Apple as the principal development language for Mac OS X after Apple acquired NeXT in 1997. Substantially simpler than other object-oriented descendants of C. Distinguished by fully dynamic method dispatch and unusual messaging syntax. Freely available implementation included in the gcc distribution (see C). On-line documentation can be found with a search at developer.apple.com/library/mac/navigation. Future development at Apple is moving to Swift (see separate entry).
OCaml 参见 Caml。
OCaml See Caml.
Occam 一种基于 CSP [ Hoa78 ] 的并发语言 [ JG89 ] ,Hoare 的表示法,用于使用受保护命令和同步发送进行基于消息的通信。该语言是 INMOS 公司的transputer处理器构建系统的首选语言,曾广泛用于欧洲。使用缩进和换行符进行句法分组。在线资源请访问wotug.org/occam/。
Occam A concurrent language [JG89] based on CSP [Hoa78], Hoare's notation for message-based communication using guarded commands and synchronization send. The language of choice for systems built from INMOS Corporation's transputer processors, once widely used in Europe. Uses indentation and line breaks for syntactic grouping. On-line resources at wotug.org/occam/.
Pascal 由 Niklaus Wirth 于 20 世纪 60 年代末设计 [ Wir71 ],主要是为了回应 Algol 68,后者被广泛认为过于臃肿。在 20 世纪 70 年代和 80 年代被广泛使用,尤其是在教学中。引入了子范围和枚举类型。统一了结构和联合。多年来,标准参考是 Wirth 与 Kathleen Jensen 合著的书 [ JW91 ];随后由 ISO 和 ANSI [ Int90 ] 标准化。免费提供的实现位于gnu-pascal.de/gpc/h-index.html。
Pascal Designed by Niklaus Wirth in the late 1960s [Wir71], largely in reaction to Algol 68, which was widely perceived as bloated. Heavily used in the 1970s and 1980s, particularly for teaching. Introduced subrange and enumeration types. Unified structures and unions. For many years, the standard reference was Wirth's book with Kathleen Jensen [JW91]; subsequently standardized by ISO and ANSI [Int90]. Freely available implementation available at gnu-pascal.de/gpc/h-index.html.
Perl 由 Larry Wall 在 20 世纪 80 年代末设计的一种通用脚本语言 [ CfWO12 ]。包括基于(扩展的)正则表达式的异常广泛的字符串操作和模式匹配机制。借用了 C、 sed、awk [ AKW88 ] 和各种 Unix shell(命令解释器)语言的功能。因能用多种方式完成几乎所有事情而闻名/臭名昭著。20 世纪 90 年代末,作为一种服务器端 Web 脚本语言,Perl 的受欢迎程度迅速飙升。第 5 版于 1995 年发布;截至 2015 年,第 6 版仍在开发中。在线资源请访问perl.org/。
Perl A general-purpose scripting language designed by Larry Wall in the late 1980s [CfWO12]. Includes unusually extensive mechanisms for character string manipulation and pattern matching based on (extended) regular expressions. Borrows features from C, sed, awk [AKW88], and various Unix shell (command interpreter) languages. Is famous/infamous for having multiple ways of doing almost anything. Enjoyed an upsurge in popularity in the late 1990s as a server-side web scripting language. Version 5 released in 1995; version 6 still under development as of 2015. On-line resources at perl.org/.
PHP Perl 的后代,设计用于服务器端 Web 脚本。脚本通常嵌入在网页中。最初由 Rasmus Lerdorf 于 1995 年创建,用于帮助管理其个人主页。现在,该名称正式采用递归缩写(PHP:超文本预处理器)。较新的版本由 Andi Gutmans 和 Zeev Suraski 与 Lerdorf 合作开发。包括对各种 Internet 协议的内置支持以及对数十种不同商业数据库系统的访问。版本 5(2004 年)添加了广泛的面向对象功能、混合继承、迭代器对象、自动加载、结构化异常处理、反射、重载和参数的可选类型声明。在线资源请访问php.net/。
PHP A descendant of Perl designed for server-side web scripting. Scripts are typically embedded in web pages. Originally created by Rasmus Lerdorf in 1995 to help manage his personal home page. The name is now officially a recursive acronym (PHP: Hypertext Preprocessor). More recent versions due to Andi Gutmans and Zeev Suraski, in cooperation with Lerdorf. Includes built-in support for a wide range of Internet protocols and for access to dozens of different commercial database systems. Version 5 (2004) added extensive object-oriented features, mix-in inheritance, iterator objects, autoloading, structured exception handling, reflection, overloading, and optional type declarations for parameters. On-line resources at php.net/.
PL/I 一种大型通用语言,设计于 20 世纪 60 年代中期,是 Fortran、Cobol 和 Algol 的后继者 [ Bee70 ]。从未取代过其前辈;主要通过 IBM 公司影响力而得以存活。
PL/I A large, general-purpose language designed in the mid-1960s as a successor to Fortran, Cobol, and Algol [Bee70]. Never managed to displace its predecessors; kept alive largely through IBM corporate influence.
后记 一种用于描述图形和打印操作的基于堆栈的语言 [ Ado90 ]。由 Adobe Systems, Inc. 开发和推广。部分基于 Forth [ Bro87 ]。由许多文字处理器和绘图程序生成。大多数专业级打印机都包含 Postscript 解释器。
Postscript A stack-based language for the description of graphics and print operations [Ado90]. Developed and marketed by Adobe Systems, Inc. Based in part on Forth [Bro87]. Generated by many word processors and drawing programs. Most professional-quality printers contain a Postscript interpreter.
Prolog 最广泛使用的逻辑编程语言。由法国艾克斯-马赛大学的 Alain Colmeraurer 和 Philippe Roussel 以及苏格兰爱丁堡大学的 Robert Kowalski 及其同事于 20 世纪 70 年代初开发。存在许多方言。1995 年部分标准化 [ Int95 ]。有许多实现,包括免费和商业实现;流行的免费版本包括 GNU Prolog ( gprolog.org/ ) 和 SWI-Prolog ( swi-prolog.org/ )。
Prolog The most widely used logic programming language. Developed in the early 1970s by Alain Colmeraurer and Philippe Roussel of the University of Aix–Marseille in France and Robert Kowalski and associates at the University of Edinburgh in Scotland. Many dialects exist. Partially standardized in 1995 [Int95]. Numerous implementations, both free and commercial, are available; popular freely available versions include GNU Prolog (gprolog.org/) and SWI-Prolog (swi-prolog.org/).
Python 一种通用的面向对象脚本语言,由 Guido van Rossum 于 20 世纪 90 年代初设计。使用缩进进行语法分组。包括动态类型、具有词法作用域的嵌套函数、lambda 表达式和高阶函数、真正的迭代器、列表推导、数组切片、反射、结构化异常处理、多重继承以及模块和动态加载。在线资源请访问python.org/。
Python A general-purpose, object-oriented scripting language designed by Guido van Rossum in the early 1990s. Uses indentation for syntactic grouping. Includes dynamic typing, nested functions with lexical scoping, lambda expressions and higher order functions, true iterators, list comprehensions, array slices, reflection, structured exception handling, multiple inheritance, and modules and dynamic loading. On-line resources at python.org/.
R 开源脚本语言,主要用于统计分析。基于专有的 S 统计编程语言,最初由贝尔实验室的 John Chambers 等人开发。支持一等和高阶函数、无限范围、按需调用、多维数组和切片以及丰富的统计函数库。在线资源请访问r-project.org/。
R Open-source scripting language intended primarily for statistical analysis. Based on the proprietary S statistical programming language, originally developed by John Chambers and others at Bell Labs. Supports first-class and higher order functions, unlimited extent, call-by-need, multidimensional arrays and slices, and an extensive library of statistical functions. On-line resources at r-project.org/.
Ruby 一种优雅、通用、面向对象的脚本语言,由 Yukihiro “Matz” Matsumoto 于 1993 年开始设计。首次发布于 1995 年。受到 Ada、Eiffel 和 Perl 的启发,带有 Python、Lisp、Clu 和 Smalltalk 的痕迹。包括动态类型、任意精度算法、真正的迭代器、用户级线程、一等函数和高阶函数、延续、反射、Smalltalk 样式的消息传递、混合继承、自动加载、结构化异常处理以及对 Tk 窗口工具包的支持。Thomas 和 Hunt 撰写的文本是标准参考 [ TFH13 ]。在线资源位于ruby-lang.org/。
Ruby An elegant, general-purpose, object-oriented scripting language designed by Yukihiro “Matz” Matsumoto, beginning in 1993. First released in 1995. Inspired by Ada, Eiffel, and Perl, with traces of Python, Lisp, Clu, and Smalltalk. Includes dynamic typing, arbitrary precision arithmetic, true iterators, user-level threads, first-class and higher order functions, continuations, reflection, Smalltalk-style messaging, mix-in inheritance, autoloading, structured exception handling, and support for the Tk windowing toolkit. The text by Thomas and Hunt is a standard reference [TFH13]. On-line resources at ruby-lang.org/.
Rust 一种用于系统编程的静态类型语言,最初由 Graydon Hoare 及其 Mozilla Research 的同事开发。语法上让人联想到 C,但更注重类型安全、内存安全(没有自动垃圾收集)和并发性。包括本地类型推断、类似 Haskell 的类型特征、泛型、混合继承、模式匹配和基于所有权转移的线程间通信。静态禁止空指针和悬空指针。在线资源请访问rust-lang.org/。
Rust A statically typed language for systems programming, initially developed by Graydon Hoare and colleagues at Mozilla Research. Syntactically reminiscent of C, but with a strong emphasis on type safety, memory safety (without automatic garbage collection), and concurrency. Includes local type inference, Haskell-like type traits, generics, mix-in inheritance, pattern matching, and inter-thread communication based on ownership transfer. Statically prohibits both null and dangling pointers. On-line resources at rust-lang.org/.
Scala 面向对象的函数式语言,由瑞士洛桑联邦理工学院的 Martin Odersky 及其同事于 2001 年开始开发。旨在在 Java 虚拟机之上实现,部分原因是 Java 中存在的缺陷。提供可以说是将函数式特性最积极地集成到命令式语言中的语言。具有一等函数和高阶函数、局部类型推断、惰性求值、模式匹配、柯里化、尾部递归、丰富的泛型(具有协变和逆变)、基于消息的并发、基于特征的继承(类和接口之间的某种交叉)。在工业界和学术界都得到了广泛使用。由欧洲研究委员会资助开发。在线资源位于scala-lang.org/。
Scala Object-oriented functional language developed by Martin Odersky and associates at the École Polytechnique Fédérale de Lausanne in Switzerland, beginning in 2001. Intended for implementation on top of the Java Virtual Machine, and motivated in part by perceived shorcomings in Java. Provides arguably the most aggressive integration of functional features into an imperative language. Has first-class and higher order functions, local type inference, lazy evaluation, pattern matching, currying, tail recursion, rich generics (with covariance and contravariance), message-based concurrency, trait-based inheritance (sort of a cross between classes and interfaces). Heavily used in both industry and academia. Development funded by the European Research Council. On-line resources at scala-lang.org/.
Scheme 一种小巧、优雅的 Lisp (另请参阅 Lisp) 方言,由 Guy Steele 和 Gerald Sussman 于 20 世纪 70 年代中期开发。具有静态作用域和真正的一等函数。广泛用于教学。第六个修订标准 (R6RS) [ SDF + 07 ] 于 2007 年发布,大大增加了语言的大小;在随后的反对意见之后,R7RS 标准 [ SCG + 13 ] 编纂了一个类似于 R5RS 版本的小型核心语言和一组更大的扩展。早期版本由 IEEE 和 ANSI [ Ins91 ] 标准化。Abelson 和 Sussman 编写的书 [ AS96 ] 长期用于 MIT 和其他地方的入门编程课程,是基本编程概念的经典指南,尤其是函数式编程。在线资源请访问community.schemewiki.org/。
Scheme A small, elegant dialect of Lisp (see also Lisp) developed in the mid-1970s by Guy Steele and Gerald Sussman. Has static scoping and true first-class functions. Widely used for teaching. The sixth revised standard (R6RS) [SDF+07], released in 2007, substantially increased the size of the language; in the wake of subsequent objections, the R7RS standard [SCG+13] codifies a small core language similar to the R5RS version and a larger set of extensions. Earlier version standardized by the IEEE and ANSI [Ins91]. Thebook by Abelson and Sussman [AS96], long used for introductory programming classes at MIT and elsewhere, is a classic guide to fundamental programming concepts, and to functional programming in particular. On-line resources at community.schemewiki.org/.
Simula 由 Ole-Johan Dahl、Bjørn Myhrhaug 和 KristenNygaard 于 20 世纪 60 年代中期在奥斯陆挪威计算中心设计 [ BDMN73、 ND78 ]。使用类和协程扩展了 Algol 60。该语言的名称反映了其适用于离散事件模拟(第 C-9.5.4 节)。directory.fsf.org/project/cim/ 提供免费的 Simula-to-C 转换器。
Simula Designed at the Norwegian Computing Center, Oslo, in the mid-1960s by Ole-Johan Dahl, Bjørn Myhrhaug, and KristenNygaard [BDMN73, ND78]. Extends Algol 60 with classes and coroutines. The name of the language reflects its suitability for discrete-event simulation (Section C-9.5.4). Free Simula-to-C translator available at directory.fsf.org/project/cim/.
单任务 C (SAC) 一种纯函数式语言,设计用于对基于数组的数据进行高性能计算 [ Sch03 ]。由 Sven-Bodo Scholz 及其同事于 1994 年开始在赫特福德大学和其他几所机构开发。与 Sisal 精神类似,但语法尽可能地基于 C。在线资源请访问 www.sac-home.org/。
Single Assignment C (SAC) A purely functional language designed for high performance computing on array-based data [Sch03]. Developed by Sven-Bodo Scholz and associates at University of Hertfordshire and several other institutions beginning in 1994. Similar in spirit to Sisal, but with syntax based as heavily as possible on C. On-line resources at www.sac-home.org/.
Sisal 一种具有“命令式”语法的函数式语言。由詹姆斯·麦格劳 (James McGraw) 及其同事在劳伦斯利弗莫尔国家实验室于 20 世纪 80 年代早期至中期开发 [ FCO90、 Can92 ]。主要用于高性能科学计算,具有自动并行化功能。数据流语言 Val [ McG82 ]的后代。LLNL 不再开发该语言;可从 sisal.sourceforge.net/ 获取开源版本。
Sisal A functional language with “imperative-style” syntax. Developed by James McGraw and associates at Lawrence Livermore National Laboratory in the early to mid-1980s [FCO90, Can92]. Intended primarily for high-performance scientific computing, with automatic parallelization. A descendant of the dataflow language Val [McG82]. No longer under development at LLNL; available open-source from sisal.sourceforge.net/.
Smalltalk 被许多人视为典型的面向对象语言。由 Alan Kay、Adele Goldberg、Dan Ingalls 及其同事在 20 世纪 70 年代在施乐帕洛阿尔托研究中心开发,最终形成了 Smalltalk-80 语言 [ GR89 ]。基于活动对象之间“消息”的拟人化编程模型。在线资源请访问smalltalk.org/。
Smalltalk Considered by many to be the quintessential object-oriented language. Developed by Alan Kay, Adele Goldberg, Dan Ingalls, and associates at the Xerox Palo Alto Research Center throughout the 1970s, culminating in the Smalltalk-80 language [GR89]. Anthropomorphic programming model based on “messages” between active objects. On-line resources at smalltalk.org/.
SML 参见 ML。
SML See ML.
Snobol 由贝尔实验室的 Ralph Griswold 及其同事于 20 世纪 60 年代开发 [ GPP71 ],最终发展为 SNOBOL4。主要用于处理字符串。包括一组极其丰富的字符串操作原语以及基于成功和失败概念的新型控制流机制。在线档案位于snobol4.org/。
Snobol Developed by Ralph Griswold and associates at Bell Labs in the 1960s [GPP71], culminating in SNOBOL4. Intended primarily for processing character strings. Included an extremely rich set of string-manipulating primitives and a novel control-flow mechanism based on the notions of success and failure. On-line archive at snobol4.org/.
SR 并发编程语言,由亚利桑那大学的 Greg Andrews 及其同事于 20 世纪 80 年代开发 [ AO93 ]。不仅集成了顺序和并发编程,还将共享内存、信号量、消息传递、远程过程和集合点集成到单一概念框架和简单语法中。在线存档位于cs.arizona.edu/sr/。
SR Concurrent programming language developed by Greg Andrews and colleagues at the University of Arizona in the 1980s [AO93]. Integrated not only sequential and concurrent programming but also shared memory, semaphores, message passing, remote procedures, and rendezvous into a single conceptual framework and simple syntax. On-line archive at cs.arizona.edu/sr/.
Swift 苹果公司开发的动态面向对象语言,是 Objective-C 的后继者。包括垃圾收集、局部类型推断、数组边界检查、关联数组、元组、具有无限范围的第一类 lambda 表达式、泛型以及对象类型的值和引用变量。在线资源请访问developer.apple.com/swift/。
Swift Dynamic object-oriented language developed by Apple as a successor to Objective-C. Includes garbage collection, local type inference, array bounds checking, associative arrays, tuples, first class lambda expressions with unlimited extent, generics, and both value and reference variables of object type. On-line resources at developer.apple.com/swift/.
Tcl/Tk 工具命令语言(发音为“tickle”)。由 John Ousterhout 于 20 世纪 80 年代末设计的脚本语言 [ Ous94、 WJH03 ]。基于关键字的语法类似于 Unix 命令行调用和开关;标点符号相对较少。使用动态作用域。支持反射、解释器的递归调用。Tk(发音为“tee-kay”)是一组用于图形用户界面 (GUI) 编程的 Tcl 命令。Tk 由 Ousterhout 设计,作为 Tcl 的扩展,还嵌入了 Ruby、Perl 和其他几种语言。在线资源请访问tcl.tk/。
Tcl/Tk Tool command language (pronounced “tickle”). Scripting language designed by John Ousterhout in the late 1980s [Ous94, WJH03]. Keyword-based syntax resembles Unix command-line invocations and switches; punctuation is relatively spare. Uses dynamic scoping. Supports reflection, recursive invocation of interpreter. Tk (pronounced “tee-kay”) is a set of Tcl commands for graphical user interface (GUI) programming. Designed by Ousterhout as an extension to Tcl, Tk has also been embedded in Ruby, Perl, and several other languages. On-line resources at tcl.tk/.
Turing 20 世纪 80 年代初,多伦多大学的 Richard Holt 及其同事从 Euclid 中衍生而来 [ HMRC88 ]。最初旨在作为一种教学语言,但可用于广泛的应用。其衍生版本也由 Holt 的团队开发,包括 Turing Plus 和面向对象的 Turing。
Turing Derived from Euclid by Richard Holt and associates at the University of Toronto in the early 1980s [HMRC88]. Originally intended as a pedagogical language, but could be used for a wide range of applications. Descendants, also developed by Holt's group, include Turing Plus and Object-Oriented Turing.
XSL 可扩展样式表语言,由万维网联盟制定标准。用作 XML(可扩展标记语言)的标准样式表语言,XML 是自描述树形结构数据的标准,XHTML 是 XML 的一种方言。包括三个子标准:XSLT(XSL 转换)[ Wor14 ],指定如何将一种 XML 方言转换为另一种方言;XPath [ Wor07 ],用于命名 XML 文档的元素;XSL-FO(XSL 格式化对象)[ Wor06b ],指定如何格式化文档。XSLT 虽然专门用于 XML 转换,但它是一种图灵完备的编程语言 [ Kep04 ]。标准和其他资源请访问w3.org/Style/XSL/。
XSL Extensible Stylesheet Language, standardized by the World Wide Web Consortium. Serves as the standard stylesheet language for XML (Extensible Markup Language), the standard for self-descriptive tree-structured data, of which XHTML is a dialect. Includes three substandards: XSLT (XSL Transformations) [Wor14], which specifies how to translate from one dialect of XML to another; XPath [Wor07], used to name elements of an XML document; and XSL-FO (XSL Formatting Objects) [Wor06b], which specifies how to format documents. XSLT, though highly specialized to the transformation of XML, is a Turing complete programming language [Kep04]. Standards and additional resources at w3.org/Style/XSL/.
在本文中,我们有机会评论了语言设计和语言实现之间的许多联系。一些更直接的联系已在单独的侧边栏中突出显示。我们在这里列出了这些侧边栏。
Throughout this text, we have had occasion to remark on the many connections between language design and language implementation. Some of the more direct connections have been highlighted in separate sidebars. We list those sidebars here.
| 第 1 章:简介 | ||
| 1.1 | 介绍 | 10 |
| 1.2 | 编译型语言和解释型语言 | 18 |
| 1.3 | Pascal 的早期成功 | 22 |
| 1.4 | 强大的开发环境 | 二十五 |
| 第 2 章:编程语言语法 | ||
| 2.1 | 上下文关键词 | 四十六 |
| 2.2 | 格式限制 | 四十八 |
| 2.3 | 嵌套注释 | 55 |
| 2.4 | 识别多种 token | 63 |
| 2.5 | 最长的 token | 64 |
| 2.6 | 悬而未决的else | 81 |
| 2.7 | 递归下降和表驱动LL解析 | 86 |
| 第 3 章:名称、范围和绑定 | ||
| 3.1 | 绑定时间 | 117 |
| 3.2 | Fortran 中的递归 | 119 |
| 3.3 | 相互递归 | 131 |
| 3.4 | 重新申报 | 134 |
| 3.5 | 模块和单独编译 | 140 |
| 3.6 | 动态作用域 | 143 |
| 3.7 | C 和 Fortran 中的指针 | 146 |
| 3.8 | OCaml 中的用户定义运算符 | 149 |
| 3.9 | 约束规则和范围 | 156 |
| 3.10 | 函数和函数对象 | 161 |
| 3.11 | 泛型作为宏 | 163 |
| 3.12 | 单独编译 | 41号 |
| 第四章:语义分析 | ||
| 4.1 | 动态语义检查 | 182 |
| 4.2 | 前向引用 | 193 |
| 4.3 | 属性赋值器 | 198 |
| 第五章:目标机架构 | ||
| 5.2 | 处理器/内存差距 | 62号 |
| 5.3 | 一兆字节是多少? | 66号 |
| 5.4 | 延迟分支指令 | 碳 90 |
| 5.5 | 延迟加载说明 | 92号 |
| 5.6 | 内联子程序 | 97号 |
| 5.1 | 伪汇编符号 | 218 |
| 第 6 章:控制流 | ||
| 6.1 | 实现参考模型 | 232 |
| 6.2 | 安全与性能 | 241 |
| 6.3 | 评估顺序 | 242 |
| 6.4 | 清理延续 | 250 |
| 6.5 | 短路评估 | 255 |
| 6.6 | 案例陈述 | 259 |
| 6.7 | 数值不精确 | 264 |
| 6.8 | for循环 | 267 |
| 6.9 | “真正的”迭代器和迭代器对象 | 270 |
| 6.10 | 正序求值 | 282 |
| 6.11 | 不确定性和公平性 | 114号 |
| 第 7 章:类型系统 | ||
| 7.1 | 系统编程 | 299 |
| 7.2 | 动态类型 | 300 |
| 7.3 | 多语言字符集 | 306 |
| 7.4 | 十进制类型 | 307 |
| 7.5 | 整数的多种大小 | 309 |
| 7.6 | 非转换类型转换 | 319 |
| 7.7 | Haskell 中重载函数的类型类 | 329 |
| 7.8 | 统一 | 331 |
| 7.9 | 机器学习中的泛型 | 334 |
| 7.10 | 重载和多态 | 336 |
| 7.11 | 为什么要擦除? | 127号 |
| 第 8 章:复合类型 | ||
| 8.1 | C 和 C++ 中的结构标签和 typedef | 353 |
| 8.2 | 记录字段的顺序 | 356 |
| 8.13 | 变体字段的放置 | 141号 |
| 8.3 | []是运算符吗? | 361 |
| 8.4 | 数组布局 | 370 |
| 8.5 | 数组索引的下限 | 373 |
| 8.6 | 指针的实现 | 378 |
| 8.7 | 堆栈粉碎 | 385 |
| 8.8 | 指针和数组 | 386 |
| 8.9 | 垃圾收集 | 390 |
| 8.10 | 垃圾到底是什么? | 393 |
| 8.11 | 引用计数与跟踪 | 396 |
| 8.12 | 汽车和cdr | 399 |
| 第 9 章:子程序和控制抽象 | ||
| 9.8 | 词汇嵌套和显示 | 164号 |
| 9.9 | 利用pc = r15 | 169号 |
| 9.10 | 在堆栈中执行代码 | 176号 |
| 9.1 | 提示和指示 | 420 |
| 9.2 | 内联和模块化 | 421 |
| 9.3 | 参数模式 | 424 |
| 9.11 | 按姓名呼叫 | 181号 |
| 9.12 | 需要的话请来电 | 182号 |
| 9.4 | 结构化异常 | 446 |
| 9.5 | 设置跳转 | 449 |
| 9.6 | 线程和协同程序 | 452 |
| 9.7 | 协程堆栈 | 453 |
| 第 10 章:数据抽象和面向对象 | ||
| 10.1 | 类声明中包括什么? | 476 |
| 10.2 | 容器/收藏品 | 484 |
| 10.3 | 价值/参考权衡 | 498 |
| 10.4 | 初始化和赋值 | 501 |
| 10.5 | “扩展”对象的初始化 | 502 |
| 10.6 | 反向赋值 | 511 |
| 10.7 | 脆弱基类问题 | 512 |
| 10.8 | 多重继承的成本 | 197号 |
| 第 11 章:函数式语言 | ||
| 11.1 | 函数式程序中的迭代 | 546 |
| 11.2 | SML 和 Haskell 中的相等性和排序 | 554 |
| 11.3 | OCaml 中的类型等价 | 560 |
| 11.4 | 惰性求值 | 570 |
| 11.5 | 单子 | 575 |
| 11.6 | 高阶函数 | 577 |
| 11.7 | 副作用和编译 | 582 |
| 第 12 章:逻辑语言 | ||
| 12.1 | 同像语言 | 608 |
| 12.2 | 反射 | 611 |
| 12.3 | 实现逻辑 | 613 |
| 12.4 | 替代搜索策略 | 614 |
| 第 13 章:并发 | ||
| 13.1 | 处理器到底是什么? | 632 |
| 13.2 | 硬件和软件通信 | 636 |
| 13.3 | 任务并行和数据并行计算 | 643 |
| 13.4 | 违反直觉的实现 | 646 |
| 13.5 | 监视信号语义 | 672 |
| 13.6 | 嵌套监控问题 | 673 |
| 13.7 | 条件临界区 | 674 |
| 13.8 | Java 中的条件变量 | 677 |
| 13.9 | 无副作用和隐式同步 | 685 |
| 13.10 | 实现问题的语义影响 | 241号 |
| 13.11 | 仿真与效率 | 243号 |
| 13.12 | 远程过程的参数 | C 250 |
| 第 14 章:脚本语言 | ||
| 14.1 | 编译解释型语言 | 702 |
| 14.2 | 规范实现 | 703 |
| 14.3 | shell 中的内置命令 | 707 |
| 14.4 | 神奇数字 | 712 |
| 14.5 | JavaScript 和 Java | 736 |
| 14.6 | 你能在多大程度上信任脚本? | 737 |
| 14.7 | W3C 和 WHATWG | 260 号 |
| 14.8 | 关于动态作用域的思考 | 742 |
| 14.9 | grep命令和 Unix 工具的诞生 | 744 |
| 14.10 | 正则表达式的自动机 | 746 |
| 14.11 | 编译正则表达式 | 749 |
| 14.12 | Perl 中的类型团 | 754 |
| 14.13 | 可执行类声明 | 763 |
| 14.14 | 越糟越好 | 764 |
| 第 15 章:构建可运行程序 | ||
| 15.1 | 后记 | 783 |
| 15.2 | 单独编译的类型检查 | 799 |
| 第 16 章:运行时程序管理 | ||
| 16.1 | 运行时系统 | 808 |
| 16.2 | 优化基于堆栈的 IF | 812 |
| 16.3 | 类文件和字节码的验证 | 820 |
| 16.4 | 假设有一个即时编译器 | 287号 |
| 16.5 | 引用和指针 | 290 号 |
| 16.6 | 模拟和解释 | 830 |
| 16.7 | 通过二进制重写创建沙箱 | 835 |
| 16.8 | 矮人 | 846 |
| 第 17 章:代码改进 | ||
| 17.1 | 窥孔优化 | C 303 |
| 17.2 | 基本块 | 304 号 |
| 17.3 | 常见子表达式 | 309号 |
| 17.4 | 指针分析 | C 310 |
| 17.5 | 循环不变量 | 324 号 |
| 17.6 | 控制流分析 | 325 号 |
| 第 1 章:简介 | ||
| 1.1 | x86 机器语言的 GCD 程序 | 5 |
| 1.2 | x86 汇编程序中的 GCD 程序 | 5 |
| 语言设计的艺术 | ||
| 编程语言范围 | ||
| 1.3 | 编程语言的分类 | 11 |
| 1.4 | C 语言中的 GCD 函数 | 十三 |
| 1.5 | OCaml 中的 GCD 函数 | 十三 |
| 1.6 | Prolog中的GCD规则 | 十三 |
| 为什么要学习编程语言? | ||
| 编译和解释 | ||
| 1.7 | 纯编译 | 17 |
| 1.8 | 纯粹的解释 | 17 |
| 1.9 | 混合编译和解释 | 18 |
| 1.10 | 预处理 | 19 |
| 1.11 | 库例程和链接 | 19 |
| 1.12 | 编译后汇编 | 20 |
| 1.13 | C 预处理器 | 20 |
| 1.14 | 源到源翻译 | 20 |
| 1.15 | 引导 | 21 |
| 1.16 | 编译解释型语言 | 23 |
| 1.17 | 动态和即时编译 | 23 |
| 1.18 | 微码(固件) | 24 |
| 编程环境 | ||
| 编译概述 | ||
| 1.19 | 编译和解释阶段 | 二十六 |
| 1.20 | C 语言中的 GCD 程序 | 二十八 |
| 1.21 | GCD 程序代币 | 二十八 |
| 1.22 | 上下文无关语法和解析 | 二十八 |
| 1.23 | GCD 程序解析树 | 二十九 |
| 1.24 | GCD 程序抽象语法树 | 33 |
| 1.25 | 解释语法树 | 33 |
| 1.26 | GCD程序汇编代码 | 三十四 |
| 1.27 | GCD程序优化 | 三十六 |
| 第 2 章:编程语言语法 | ||
| 2.1 | 阿拉伯数字的语法 | 43 |
| 指定语法:正则表达式和上下文无关文法 | ||
| 2.2 | C11 的词汇结构 | 四十五 |
| 2.3 | 数字常量的语法 | 四十六 |
| 2.4 | 表达式中的语法嵌套 | 四十八 |
| 2.5 | 扩展 BNF(EBNF) | 49 |
| 2.6 | 斜率 * x + 截距的推导 | 50 |
| 2.7 | 解析斜率 * x + 截距的树 | 51 |
| 2.8 | 具有优先级和结合性的表达式语法 | 52 |
| 扫描 | ||
| 2.9 | 计算器语言的标记 | 54 |
| 2.10 | 计算器令牌的临时扫描器 | 54 |
| 2.11 | 计算器扫描仪的有限自动机 | 55 |
| 2.12 | 为给定的正则表达式构建 NFA | 58 |
| 2.13 | d * (. d | d . ) d * 的NFA | 59 |
| 2.14 | d * ( . d | d . ) d *的 DFA | 60 |
| 2.15 | d *( . d | d . ) d *的最小 DFA | 60 |
| 2.16 | 嵌套case语句自动机 | 62 |
| 2.17 | 非平凡前缀问题 | 64 |
| 2.18 | Fortran 扫描中的前瞻 | 64 |
| 2.19 | 表格驱动扫描 | 65 |
| 解析 | ||
| 2.20 | 自上而下和自下而上的解析 | 70 |
| 2.21 | 使用自下而上的语法来限制空间 | 72 |
| 2.22 | 自上而下的计算器语言语法 | 73 |
| 2.23 | 计算器语言的递归下降解析器 | 75 |
| 2.24 | “求和与平均”程序的递归下降解析 | 75 |
| 2.25 | 左递归 | 79 |
| 2.26 | 常见前缀 | 79 |
| 2.27 | 消除左递归 | 80 |
| 2.28 | 左分解 | 80 |
| 2.29 | 解析“悬垂 else” | 80 |
| 2.30 | “悬而未决”程序错误 | 81 |
| 2.31 | 结构化语句的结束标记 | 81 |
| 2.32 | elsif的必要性 | 82 |
| 2.33 | 自上而下解析的驱动程序和表 | 82 |
| 2.34 | “求和与平均”程序的表驱动解析 | 83 |
| 2.35 | 计算器语言的预测集 | 84 |
| 2.36 | 导出id列表 | 90 |
| 2.37 | 计算器语言的自下而上的语法 | 90 |
| 2.38 | 自下而上解析“求和与平均”程序 | 91 |
| 2.39 | 自下而上的计算器语法的 CFSM | 95 |
| 2.40 | 自下而上的计算器语法中的 Epsilon 产生式 | 95 |
| 2.41 | 带有 epsilon 生成的 CFSM | 101 |
| 2.42 | C 中的语法错误 | 102 |
| 2.43 | C 中的语法错误(重复) | 1 号 |
| 2.44 | 恐慌模式的问题 | 1 号 |
| 2.45 | 递归下降中的短语级恢复 | 碳2 |
| 2.46 | 级联语法错误 | 碳3 |
| 2.47 | 通过特定上下文的前瞻减少级联错误 | 碳4 |
| 2.48 | 具有完整短语级恢复的递归下降 | 碳4 |
| 2.49 | 递归下降解析器中的异常 | 碳5 |
| 2.50 | “ ; else “的错误产生 | 碳6 |
| 2.51 | FMQ 中的仅插入修复 | 碳8 |
| 2.52 | 有删除内容的 FMQ | 碳8 |
| 2.53 | yacc/bison中的恐慌模式 | 11号 |
| 2.54 | 带有语句终止符的恐慌模式 | 11号 |
| 2.55 | yacc/bison中的短语级恢复 | 11号 |
| 理论基础 | ||
| 2.56 | d * ( . d | d . ) d *的正式 DFA | 14号 |
| 2.57 | 为十进制字符串 DFA 重建正则表达式 | 15号 |
| 2.58 | 具有大型最小 DFA 的正则语言 | 16号 |
| 2.59 | 指数 DFA 爆炸 | 16号 |
| 2.60 | 0 n 1 n不是常规语言 | 19号 |
| 2.61 | 语法分类 | 碳 20 |
| 2.62 | 语言课程分离 | 碳 20 |
| 第 3 章:名称、范围和绑定 | ||
| 绑定时间的概念 | ||
| 对象生命周期和存储管理 | ||
| 3.1 | 局部变量的静态分配 | 119 |
| 3.2 | 运行时堆栈的布局 | 120 |
| 3.3 | 堆中的外部碎片 | 122 |
| 范围规则 | ||
| 3.4 | C 中的静态变量 | 127 |
| 3.5 | 嵌套作用域 | 128 |
| 3.6 | 静态链 | 130 |
| 3.7 | 在声明前使用中的“陷阱” | 131 |
| 3.8 | C# 中的整个块范围 | 132 |
| 3.9 | Python 中的“本地化” | 132 |
| 3.10 | Scheme 中的声明顺序 | 132 |
| 3.11 | C 语言中的声明与定义 | 133 |
| 3.12 | C 中的内部声明 | 134 |
| 3.13 | 伪随机数作为模块的动机 | 136 |
| 3.14 | C++ 中的伪随机数生成器 | 137 |
| 3.15 | 模块作为类型的“管理器” | 138 |
| 3.16 | 伪随机数生成器类型 | 140 |
| 3.17 | 大型应用程序中的模块和类 | 142 |
| 3.18 | 静态与动态作用域 | 143 |
| 3.19 | 具有动态作用域的运行时错误 | 144 |
| 实现范围 | ||
| 3.45 | LeBlanc-Cook 符号表 | 27号 |
| 3.46 | 示例程序的符号表 | 28号 |
| 3.47 | Lisp 中的 A 列表查找 | 31号 |
| 3.48 | 中央参考表 | 33号 |
| 3.49 | 顶级酒店倒闭 | 33号 |
| 范围内名称的含义 | ||
| 3.20 | 使用参数进行别名 | 146 |
| 3.21 | 别名和代码改进 | 146 |
| 3.22 | Ada 中的重载枚举常量 | 147 |
| 3.23 | 解决歧义重载 | 147 |
| 3.24 | C++ 中的重载 | 148 |
| 3.25 | Ada 中的运算符重载 | 148 |
| 3.26 | C++ 中的运算符重载 | 148 |
| 3.27 | Haskell 中的中缀运算符 | 149 |
| 3.28 | 使用类型类进行重载 | 149 |
| 3.29 | 打印多种类型的对象 | 150 |
| 引用环境的绑定 | ||
| 3.30 | 深浅绑定 | 152 |
| 3.31 | 具有静态作用域的绑定规则 | 154 |
| 3.32 | 在 Scheme 中返回一等子程序 | 156 |
| 3.33 | Java 中的对象闭包 | 157 |
| 3.34 | C# 中的委托 | 158 |
| 3.35 | 代表和无限范围 | 158 |
| 3.36 | C++ 中的函数对象 | 158 |
| 3.37 | C# 中的 lambda 表达式 | 159 |
| 3.38 | 多种 lambda 语法 | 159 |
| 3.39 | C++11 中的简单 lambda 表达式 | 160 |
| 3.40 | C++ lambda 表达式中的变量捕获 | 161 |
| 3.41 | Java 8 中的 Lambda 表达式 | 162 |
| 宏扩展 | ||
| 3.42 | 一个简单的汇编宏 | 162 |
| 3.43 | C 中的预处理器宏 | 163 |
| 3.44 | C 宏中的“陷阱” | 163 |
| 单独编译 | ||
| 3.50 | C++ 中的命名空间 | 39号 |
| 3.51 | 使用另一个命名空间中的名称 | 39号 |
| 3.52 | Java 中的包 | C 40 |
| 3.53 | 使用另一个包中的名称 | C 40 |
| 3.54 | 由多部分组成的包名称 | 41号 |
| 第四章:语义分析 | ||
| 语义分析器的作用 | ||
| 4.1 | Java 中的断言 | 182 |
| 4.2 | C 中的断言 | 183 |
| 属性文法 | ||
| 4.3 | 自下而上的常量表达式 CFG | 184 |
| 4.4 | 自下而上的 AG 用于常量表达式 | 185 |
| 4.5 | 自上而下 AG 计算列表元素数量 | 185 |
| 评估属性 | ||
| 4.6 | 解析树的装饰 | 187 |
| 4.7 | 自上而下的 CFG 和解析树进行减法 | 188 |
| 4.8 | 使用从左到右属性流进行装饰 | 188 |
| 4.9 | 自上而下的 AG 减法 | 189 |
| 4.10 | 自上而下的常量表达式 AG | 189 |
| 4.11 | 自下而上和自上而下的 AG 构建语法树 | 193 |
| 行动惯例 | ||
| 4.12 | 自上而下的操作程序构建语法树 | 198 |
| 4.13 | 递归下降和动作例程 | 199 |
| 属性的空间管理 | ||
| 4.19 | 自下而上解析的堆栈跟踪,带有操作例程 | 45号 |
| 4.20 | 在“埋藏的”记录中寻找继承的属性 | 46号 |
| 4.21 | 需要上下文的语法片段 | 47号 |
| 4.22 | 上下文的语义钩子 | 47号 |
| 4.23 | 破坏 LR CFG 的语义钩子 | 48号 |
| 4.24 | 尾部的动作例程 | 49号 |
| 4.25 | 用左因子代替语义钩子 | 49号 |
| 4.26 | LL 属性堆栈的操作 | C 50 |
| 4.27 | 语义堆栈的临时管理 | 53号 |
| 4.28 | 使用属性堆栈处理列表 | 54号 |
| 4.29 | 使用语义堆栈处理列表 | 55号 |
| 树文法和语法树装饰 | ||
| 4.14 | 具有类型的计算器语言的自下而上的 CFG | 201 |
| 4.15 | 对整数和实数取平均值的语法树 | 201 |
| 4.16 | 具有类型的计算器语言的树语法 | 201 |
| 4.17 | Tree AG 是一种具有类型的计算器语言 | 203 |
| 4.18 | 使用示例 4.17 中的 AG 装饰一棵树 | 206 |
| 第五章:目标机架构 | ||
| 内存层次结构 | ||
| 5.1 | 内存层次统计 | 61号 |
| 数据表示 | ||
| 5.2 | 大端和小端 | 63号 |
| 5.3 | 十六进制数 | 65号 |
| 5.4 | 二进制补码 | 66号 |
| 5.5 | 二进制补码加法溢出 | 66号 |
| 5.6 | 有偏指数 | 68号 |
| 5.7 | IEEE 浮点 | 68号 |
| 指令集架构 (ISA) | ||
| 5.8 | x86 汇编程序中的if语句 | 72号 |
| 5.9 | 比较和测试指令 | 73号 |
| 5.10 | 有条件的举动 | 73号 |
| 架构与实现 | ||
| 5.11 | x86 ISA | 碳 80 |
| 5.12 | ARM ISA | 81号 |
| 5.13 | x86 和 ARM 寄存器集 | 82号 |
| 针对现代处理器进行编译 | ||
| 5.14 | 性能≠时钟频率 | C.88 |
| 5.15 | 填充加载延迟槽 | 91号 |
| 5.16 | 重命名寄存器以进行调度 | 92号 |
| 5.17 | 简单循环的寄存器分配 | 93号 |
| 5.18 | 寄存器分配和指令调度 | 95 号 |
| 第 6 章:控制流 | ||
| 表达式求值 | ||
| 6.1 | 典型的函数调用 | 225 |
| 6.2 | 典型运算符 | 225 |
| 6.3 | 剑桥波兰语(前缀)表示法 | 225 |
| 6.4 | 机器学习中的并置 | 225 |
| 6.5 | Smalltalk 中的混合符号 | 226 |
| 6.6 | 条件表达式 | 226 |
| 6.7 | 复杂的 Fortran 表达式 | 226 |
| 6.8 | 四种有影响力的语言的优先地位 | 227 |
| 6.9 | Pascal 优先级中的“陷阱” | 227 |
| 6.10 | 结合性的通用规则 | 227 |
| 6.11 | Haskell 中的用户定义优先级和结合性 | 228 |
| 6.12 | L 值和 r 值 | 230 |
| 6.13 | C 中的 L 值 | 230 |
| 6.14 | C++ 中的 L 值 | 231 |
| 6.15 | 变量作为值和引用 | 231 |
| 6.16 | 包装器类 | 232 |
| 6.17 | Java 5 和 C# 中的装箱 | 232 |
| 6.18 | Algol 68 中的表达方向 | 233 |
| 6.19 | C 条件中的“陷阱” | 234 |
| 6.20 | 更新作业 | 234 |
| 6.21 | 副作用和更新 | 235 |
| 6.22 | 赋值运算符 | 235 |
| 6.23 | 前缀和后缀增加/减少 | 235 |
| 6.24 | 后缀 inc/dec 的优点 | 236 |
| 6.25 | 简单多路分配 | 236 |
| 6.26 | 多路分配的优点 | 236 |
| 6.27 | 明确分配禁止的程序 | 239 |
| 6.28 | 不确定的顺序 | 240 |
| 6.29 | 取决于顺序的值 | 241 |
| 6.30 | 依赖于排序的优化 | 241 |
| 6.31 | 优化和数学“定律” | 242 |
| 6.32 | 溢出和算术“恒等式” | 243 |
| 6.33 | 重新排序和数值稳定性 | 243 |
| 6.34 | 短路表达式 | 243 |
| 6.35 | 通过短路节省时间 | 243 |
| 6.36 | 短路指针追逐 | 244 |
| 6.37 | 短路和其他错误 | 244 |
| 6.38 | 可选短路 | 245 |
| 结构化和非结构化流程 | ||
| 6.39 | 在 Fortran 中使用 goto 进行控制流 | 246 |
| 6.40 | 退出嵌套子程序 | 247 |
| 6.41 | 结构化非本地转移 | 248 |
| 6.42 | 使用状态代码进行错误检查 | 249 |
| 6.43 | 一个简单的 Ruby 延续 | 250 |
| 6.44 | 延续重用和无限范围 | 251 |
| 测序 | ||
| 6.45 | 随机数生成器的副作用 | 252 |
| 选择 | ||
| 6.46 | Algol 60 中的选择 | 253 |
| 6.47 | elsif/elif | 253 |
| 6.48 | Lisp 中的cond | 253 |
| 6.49 | 布尔条件的代码生成 | 254 |
| 6.50 | 短路代码生成 | 255 |
| 6.51 | 布尔值的短路创建 | 255 |
| 6.52 | case语句和嵌套if | 256 |
| 6.53 | 嵌套if的翻译 | 257 |
| 6.54 | 跳转表 | 257 |
| 6.55 | C 语言 switch 语句中的 fall-through | 260 |
| 迭代 | ||
| 6.56 | Fortran 90循环 | 262 |
| 6.57 | Modula-2 for循环 | 262 |
| 6.58 | for循环的明显翻译 | 262 |
| 6.59 | 循环翻译,底部有测试 | 263 |
| 6.60 | 具有迭代计数的for循环翻译 | 263 |
| 6.61 | 幼稚循环翻译中的“陷阱” | 263 |
| 6.62 | 在for循环中更改索引 | 265 |
| 6.63 | 检查for循环后的索引 | 265 |
| 6.64 | C 中的组合(for )循环 | 267 |
| 6.65 | 带有本地索引的C for循环 | 268 |
| 6.66 | Python 中的简单迭代器 | 268 |
| 6.67 | 用于树枚举的 Python 迭代器 | 269 |
| 6.68 | 用于树枚举的 Java 迭代器 | 270 |
| 6.69 | C++11 中的迭代 | 270 |
| 6.70 | 将“循环体”传递给 Scheme 中的迭代器 | 272 |
| 6.71 | Smalltalk 中的块迭代 | 272 |
| 6.72 | 在 Ruby 中使用 procs 进行迭代 | 273 |
| 6.73 | 在 C 中模仿迭代器 | 274 |
| 6.89 | Icon 中的简单生成器 | 107号 |
| 6.90 | 表达式内的生成器 | 107号 |
| 6.91 | 寻求成功的生成 | C 108 |
| 6.92 | 使用多个生成器进行回溯 | C 108 |
| 6.74 | Algol-W 中的while循环 | 275 |
| 6.75 | Pascal 和 Modula 中的后测试循环 | 275 |
| 6.76 | C 语言中的后测试循环 | 275 |
| 6.77 | C 中的break语句 | 276 |
| 6.78 | 在 Ada 中退出嵌套循环 | 276 |
| 6.79 | 在 Perl 中退出嵌套循环 | 276 |
| 递归 | ||
| 6.80 | 一个“自然迭代”的问题 | 278 |
| 6.81 | 一个“自然递归”的问题 | 278 |
| 6.82 | 用另一种方式解决问题 | 278 |
| 6.83 | 尾递归的迭代实现 | 279 |
| 6.84 | 手动创建尾递归代码 | 279 |
| 6.85 | 朴素递归斐波那契函数 | 280 |
| 6.86 | 线性迭代斐波那契函数 | 281 |
| 6.87 | 高效的尾递归斐波那契函数 | 281 |
| 6.88 | 无限数据结构的惰性求值 | 283 |
| 6.93 | 避免非确定性不对称 | 110 号 |
| 6.94 | 使用保护命令进行选择 | 110 号 |
| 6.95 | 使用受保护的命令进行循环 | 111号 |
| 6.96 | 不确定的消息接收 | 112号 |
| 6.97 | SR 中的非确定性服务器 | 112号 |
| 6.98 | 非确定性的简单(不公平)实现 | 113号 |
| 6.99 | 非确定性循环实现中的“陷阱” | 113号 |
| 第 7 章:类型系统 | ||
| 7.1 | 利用类型信息的操作 | 297 |
| 7.2 | 通过类型信息捕获的错误 | 297 |
| 7.3 | 类型作为“可能别名”信息的来源 | 298 |
| 概述 | ||
| 7.4 | void(简单)类型 | 303 |
| 7.5 | 不使用void进行操作 | 303 |
| 7.6 | OCaml 中的选项类型 | 303 |
| 7.7 | Swift 中的选项类型 | 304 |
| 7.8 | Ada 中的聚合 | 304 |
| 7.9 | Pascal 中的枚举 | 307 |
| 7.10 | 枚举作为常量 | 308 |
| 7.11 | 枚举类型与枚举类型的相互转换 | 308 |
| 7.12 | 枚举的杰出值 | 308 |
| 7.13 | 在 Java 中模拟不同的枚举值 | 309 |
| 7.14 | Pascal 中的子范围 | 309 |
| 7.15 | Ada 中的子范围 | 310 |
| 7.16 | 子范围类型的空间要求 | 310 |
| 类型检查 | ||
| 7.17 | 类型的细微差异 | 313 |
| 7.18 | 类型的其他细微差异 | 313 |
| 7.19 | 结构等价问题 | 314 |
| 7.20 | 别名类型 | 314 |
| 7.21 | 语义等效的别名类型 | 315 |
| 7.22 | 语义上不同的别名类型 | 315 |
| 7.23 | Ada 中的派生类型和子类型 | 315 |
| 7.24 | 名称与结构等价性 | 316 |
| 7.25 | 需要给定类型的上下文 | 316 |
| 7.26 | Ada 中的类型转换 | 317 |
| 7.27 | C 中的类型转换 | 318 |
| 7.28 | Ada 中未经检查的转换 | 319 |
| 7.29 | C++ 中的转换和非转换强制类型转换 | 319 |
| 7.30 | C 中的强制转换 | 320 |
| 7.31 | 强制转换与加数重载 | 322 |
| 7.32 | Java 对象容器 | 323 |
| 7.33 | 子范围类型的推断 | 324 |
| 7.34 | 集合的类型推断 | 325 |
| 7.35 | C# 中的var声明 | 325 |
| 7.36 | 避免混乱的声明 | 325 |
| 7.37 | C++11 中的decltype | 326 |
| 7.38 | OCaml 中的斐波那契函数 | 327 |
| 7.39 | 使用显式类型检查 | 327 |
| 7.40 | 表达式类型 | 328 |
| 7.41 | 类型不一致 | 328 |
| 7.42 | 多态函数 | 329 |
| 7.43 | 统一的一个简单例子 | 330 |
| 参数多态性 | ||
| 7.44 | 在 OCaml 或 Haskell 中查找最小值 | 331 |
| 7.45 | Scheme 中的隐式多态性 | 332 |
| 7.46 | Ruby 中的鸭子类型 | 332 |
| 7.47 | Ada 中的通用min函数 | 333 |
| 7.48 | C++ 中的通用队列 | 333 |
| 7.49 | 通用参数 | 333 |
| 7.50 | 使用Ada 中的约束 | 336 |
| 7.51 | Java 中的通用排序例程 | 336 |
| 7.52 | C# 中的通用排序例程 | 337 |
| 7.53 | C++ 中的通用排序例程 | 337 |
| 7.54 | C++ 中的泛型类实例 | 338 |
| 7.55 | Ada 中的通用子程序实例 | 338 |
| 7.56 | C++ 中的隐式实例 | 338 |
| 7.58 | C++ 中的通用仲裁器类 | 119号 |
| 7.59 | 模板函数体移至 .c 文件 | 121号 |
| 7.60 | C++11 中的外部模板 | 122号 |
| 7.61 | C++ 模板中的实例化时错误 | 122号 |
| 7.62 | Java 中的通用Arbiter类 | 124号 |
| 7.63 | Java 泛型参数的通配符和界限 | C 125 |
| 7.64 | 类型擦除和隐式强制类型转换 | 126号 |
| 7.65 | Java 中的未经检查的警告 | 127号 |
| 7.66 | Java 泛型和内置类型 | 127号 |
| 7.67 | 在 C# 中共享泛型实现 | 128号 |
| 7.68 | C# 泛型和内置类型 | 128号 |
| 7.69 | C# 中的通用Arbiter类 | 128号 |
| 7.70 | Arbiter接口中的逆变 | 128号 |
| 7.71 | 协方差 | 130 号 |
| 7.72 | 选择者作为代表 | 130 号 |
| 相等性测试和分配 | ||
| 7.57 | Scheme 中的相等性测试 | 340 |
| 第 8 章:复合类型 | ||
| 记录(结构) | ||
| 8.1 | 交流结构 | 352 |
| 8.2 | 访问记录字段 | 352 |
| 8.3 | 嵌套记录 | 352 |
| 8.4 | OCaml 记录和元组 | 353 |
| 8.5 | 记录类型的内存布局 | 353 |
| 8.6 | 嵌套记录作为值 | 354 |
| 8.7 | 嵌套记录作为引用 | 354 |
| 8.8 | 打包类型的布局 | 354 |
| 8.9 | 记录的分配和比较 | 355 |
| 8.10 | 通过对字段进行排序来最小化漏洞 | 356 |
| 8.11 | C 语言中的 union | 357 |
| 8.12 | 变体记录的动机 | 358 |
| 8.59 | 传统 C 中的嵌套结构和联合 | 136号 |
| 8.60 | Pascal 中的变体记录 | 137号 |
| 8.61 | C11 和 C++11 中的匿名联合 | 137号 |
| 8.62 | 使用 union 来破坏类型安全 | 138号 |
| 8.63 | OCaml 中的类型安全联合 | 139号 |
| 8.64 | Ada 变体和标签(判别式) | C 140 |
| 8.65 | Ada 中的可区分子类型 | 141号 |
| 8.66 | Ada 中的判别数组 | 141号 |
| 8.67 | 派生类型作为联合的替代 | 142号 |
| 数组 | ||
| 8.13 | 数组声明 | 359 |
| 8.14 | 多维数组 | 360 |
| 8.15 | 多维数组与组合数组 | 360 |
| 8.16 | C 中的数组的数组 | 361 |
| 8.17 | 数组切片操作 | 362 |
| 8.18 | C 语言中动态形状的局部数组 | 365 |
| 8.19 | 详细数组的堆栈分配 | 365 |
| 8.20 | Fortran 90 中的精细数组 | 366 |
| 8.21 | Java 和 C# 中的动态字符串 | 367 |
| 8.22 | 行主序与列主序数组布局 | 368 |
| 8.23 | 阵列布局和缓存性能 | 368 |
| 8.24 | 连续与行指针数组布局 | 370 |
| 8.25 | 索引连续数组 | 371 |
| 8.26 | 数组索引的静态部分和动态部分 | 372 |
| 8.27 | 索引复杂结构 | 373 |
| 8.28 | 索引行指针数组 | 374 |
| 字符串 | ||
| 8.29 | C 和 C++ 中的字符转义 | 375 |
| 8.30 | C 语言中的char*赋值 | 376 |
| 套 | ||
| 8.31 | Pascal 中的集合类型 | 376 |
| 8.32 | 在 Go 中使用 map 模拟集合 | 377 |
| 指针和递归类型 | ||
| 8.33 | OCaml 中的树类型 | 379 |
| 8.34 | Lisp 中的树类型 | 379 |
| 8.35 | OCaml 中的相互递归类型 | 380 |
| 8.36 | Ada 和 C 中的树类型 | 382 |
| 8.37 | 分配堆节点 | 382 |
| 8.38 | 面向对象堆节点分配 | 382 |
| 8.39 | 基于指针的树 | 382 |
| 8.40 | 指针取消引用 | 382 |
| 8.41 | Ada 中的隐式解除引用 | 383 |
| 8.42 | OCaml 中的指针取消引用 | 383 |
| 8.43 | Lisp 中的赋值 | 384 |
| 8.44 | C 中的数组名称和指针 | 384 |
| 8.45 | C 语言中的指针比较和减法 | 386 |
| 8.46 | C 中的指针和数组声明 | 386 |
| 8.47 | C 语言中的数组作为参数 | 387 |
| 8.48 | C 语言中的sizeof | 387 |
| 8.49 | 显式存储回收 | 388 |
| 8.50 | 在 C++ 中对堆栈变量的悬垂引用 | 388 |
| 8.51 | 在 C++ 中对堆变量的悬垂引用 | 388 |
| 8.68 | 使用墓碑检测悬空引用 | 144号 |
| 8.69 | 使用锁和钥匙进行悬垂参考检测 | 146号 |
| 8.52 | 引用计数和循环结构 | 391 |
| 8.53 | 带指针反转的堆跟踪 | 394 |
| 列表 | ||
| 8.54 | ML 和 Lisp 中的列表 | 398 |
| 8.55 | 列表符号 | 399 |
| 8.56 | Lisp 中的基本列表操作 | 400 |
| 8.57 | OCaml 中的基本列表操作 | 400 |
| 8.58 | 列表推导 | 400 |
| 8.70 | 文件作为内置类型 | C 150 |
| 8.71 | 开放操作 | C 150 |
| 8.72 | 关闭操作 | C 150 |
| 8.73 | Fortran 中的格式化输出 | 152号 |
| 8.74 | 标签格式 | 152号 |
| 8.75 | 打印到标准输出 | 153号 |
| 8.76 | Ada 中的格式化输出 | 153号 |
| 8.77 | 重载put例程 | 154号 |
| 8.78 | C 中的格式化输出 | 154号 |
| 8.79 | 格式字符串中的文本 | 155号 |
| 8.80 | C 中的格式化输入 | 155号 |
| 8.81 | C++ 中的格式化输出 | 156号 |
| 8.82 | 流操纵器 | 157号 |
| 8.83 | C++ 中的数组输出 | 157号 |
| 8.84 | 更改默认格式 | 158号 |
| 第 9 章:子程序和控制抽象 | ||
| 回顾堆栈布局 | ||
| 9.1 | 运行时堆栈的布局(重复) | 412 |
| 9.2 | 相对于帧指针的偏移量 | 412 |
| 9.3 | 静态和动态链接 | 412 |
| 9.4 | 嵌套例程的可见性 | 413 |
| 调用序列 | ||
| 9.5 | 典型的调用序列 | 415 |
| 9.56 | 使用显示器进行非本地访问 | 163号 |
| 9.57 | LLVM/ARM 堆栈布局 | 167号 |
| 9.58 | LLVM/ARM 调用序列 | C 170 |
| 9.59 | gcc /x86-32 堆栈布局 | 172号 |
| 9.60 | gcc /x86-32 调用序列 | 172号 |
| 9.61 | 子程序闭包蹦床 | 174号 |
| 9.62 | x86-64 红区 | 175号 |
| 9.63 | 在 SPARC 上注册窗口 | 177号 |
| 9.6 | 请求内联子程序 | 419 |
| 9.7 | 内联和递归 | 420 |
| 参数传递 | ||
| 9.8 | 中缀运算符 | 422 |
| 9.9 | Lisp 和 Smalltalk 中的控制抽象 | 422 |
| 9.10 | 将参数传递给子程序 | 423 |
| 9.11 | 值及参考参数 | 423 |
| 9.12 | 按值/结果调用 | 424 |
| 9.13 | 在 C 中模拟引用调用 | 424 |
| 9.14 | C 中的const参数 | 426 |
| 9.15 | C++ 中的引用参数 | 428 |
| 9.16 | C++ 中的引用作为别名 | 428 |
| 9.17 | 使用内联别名简化代码 | 428 |
| 9.18 | 从函数返回引用 | 429 |
| 9.19 | C++11 中的 R 值引用 | 430 |
| 9.20 | Ada 中的子程序作为参数 | 431 |
| 9.21 | Scheme 中的一等子程序 | 431 |
| 9.22 | OCaml 中的一等子程序 | 432 |
| 9.23 | C 和 C++ 中的子程序指针 | 432 |
| 9.64 | Jensen 的设备 | C 180 |
| 9.24 | Ada 中的默认参数 | 433 |
| 9.25 | Ada 中的命名参数 | 435 |
| 9.26 | 使用命名参数进行自我文档化 | 436 |
| 9.27 | C 语言中可变数量的参数 | 436 |
| 9.28 | Java 中可变数量的参数 | 437 |
| 9.29 | C# 中可变数量的参数 | 438 |
| 9.30 | return语句 | 438 |
| 9.31 | 返回值的增量计算 | 438 |
| 9.32 | Go 中明确命名的返回值 | 439 |
| 9.33 | 多值返回 | 439 |
| 异常处理 | ||
| 9.34 | PL/I 中的ON条件 | 441 |
| 9.35 | C++ 中的简单try块 | 441 |
| 9.36 | 嵌套try块 | 442 |
| 9.37 | 将异常从被调用例程中传播出去 | 442 |
| 9.38 | 什么是例外? | 444 |
| 9.39 | 参数化异常 | 444 |
| 9.40 | C++ 中的多个处理程序 | 445 |
| 9.41 | OCaml 中的异常处理程序 | 446 |
| 9.42 | Python 中的finally子句 | 447 |
| 9.43 | 堆叠异常处理程序 | 447 |
| 9.44 | 每个处理程序有多个异常 | 447 |
| 9.45 | C 中的setjmp和longjmp | 449 |
| 协程 | ||
| 9.46 | 显式交错并发计算 | 451 |
| 9.47 | 交错协程 | 451 |
| 9.48 | 仙人掌堆 | 453 |
| 9.49 | 切换协程 | 455 |
| 9.65 | 基于协程的迭代器调用 | 183号 |
| 9.66 | 基于协程的迭代器实现 | 183号 |
| 9.67 | C# 中的迭代器使用 | 184号 |
| 9.68 | C# 迭代器的实现 | 185号 |
| 9.69 | 复杂物理系统的顺序模拟 | 187号 |
| 9.70 | 基于协程的交通模拟的初始化 | 187号 |
| 9.71 | 在交通模拟中穿越街道段 | 188号 |
| 9.72 | 安排协程以供将来执行 | 188号 |
| 9.73 | 红绿灯处车辆排队 | 188号 |
| 9.74 | 等红灯 | 189号 |
| 9.75 | 为未来的执行而睡觉 | 189号 |
| 活动 | ||
| 9.50 | 信号蹦床 | 457 |
| 9.51 | C# 中的事件处理程序 | 459 |
| 9.52 | 匿名委托处理程序 | 459 |
| 9.53 | Java 中的事件处理程序 | 460 |
| 9.54 | 匿名内部类处理程序 | 460 |
| 9.55 | 使用 lambda 表达式处理事件 | 460 |
| 第 10 章:数据抽象和面向对象 | ||
| 面向对象编程 | ||
| 10.1 | C++ 中的list_node类 | 473 |
| 10.2 | 使用list_node的列表类 | 473 |
| 10.3 | 内联(扩展)对象的声明 | 475 |
| 10.4 | 构造函数参数 | 475 |
| 10.5 | 方法声明无定义 | 476 |
| 10.6 | 单独的方法定义 | 477 |
| 10.7 | C# 中的属性和索引器方法 | 477 |
| 10.8 | 从列表派生的队列类 | 478 |
| 10.9 | 隐藏基类的成员 | 479 |
| 10.10 | 在派生类中重新定义方法 | 479 |
| 10.11 | 访问基类成员 | 480 |
| 10.12 | 在 Eiffel 中重命名方法 | 480 |
| 10.13 | 包含列表的队列 | 480 |
| 10.14 | 通用列表的基类 | 481 |
| 10.15 | 类型特定扩展的问题 | 482 |
| 10.16 | 如何命名未知类型? | 483 |
| 10.17 | C++ 中的泛型列表 | 483 |
| 封装和继承 | ||
| 10.18 | Ada 中的数据隐藏 | 486 |
| 10.19 | 隐藏this参数 | 487 |
| 10.20 | 隐藏继承的方法 | 488 |
| 10.21 | C++ 中的受保护基类 | 488 |
| 10.22 | Java 中的内部类 | 490 |
| 10.23 | Ada 2005 中的列表和队列抽象 | 491 |
| 10.24 | C# 中的扩展方法 | 494 |
| 初始化和终止 | ||
| 10.25 | 在 Eiffel 中命名构造函数 | 496 |
| 10.26 | Smalltalk 中的元类 | 497 |
| 10.27 | C++ 中的声明和构造函数 | 498 |
| 10.28 | 复制构造函数 | 499 |
| 10.29 | 临时对象 | 499 |
| 10.30 | 返回值优化 | 500 |
| 10.31 | Eiffel 构造函数和扩展对象 | 501 |
| 10.32 | 基类构造函数参数的规范 | 502 |
| 10.33 | 成员构造函数参数的规范 | 502 |
| 10.34 | 构造函数转发 | 503 |
| 10.35 | Java 中基类构造函数的调用 | 503 |
| 10.36 | 使用析构函数回收空间 | 504 |
| 动态方法绑定 | ||
| 10.37 | 基类上下文中的派生类对象 | 505 |
| 10.38 | 静态和动态方法绑定 | 506 |
| 10.39 | 动态绑定的必要性 | 507 |
| 10.40 | C++ 和 C# 中的虚方法 | 508 |
| 10.41 | Ada 95 中的类范围类型 | 508 |
| 10.42 | Java 和 C# 中的抽象方法 | 508 |
| 10.43 | C++ 中的抽象方法 | 509 |
| 10.44 | 虚表 | 509 |
| 10.45 | 虚拟方法调用的实现 | 509 |
| 10.46 | 单继承的实现 | 510 |
| 10.47 | C++ 中的强制类型转换 | 511 |
| 10.48 | Eiffel 和 C# 中的反向赋值 | 511 |
| 10.49 | 对象闭包中的虚方法 | 513 |
| 10.50 | 封装参数 | 514 |
| 混合继承 | ||
| 10.51 | 接口的动机 | 516 |
| 10.52 | 将接口混合到派生类中 | 516 |
| 10.53 | 混合继承的编译时实现 | 517 |
| 10.54 | 使用默认方法 | 520 |
| 10.55 | 默认方法的实现 | 520 |
| 10.56 | 从两个基类派生 | 521 |
| 10.57 | 从两个基类派生(重复) | 194号 |
| 10.58 | (非重复)多重继承 | 194号 |
| 10.59 | 具有多重继承的方法调用 | 195 号 |
| 10.60 | 此修正 | 196号 |
| 10.61 | 在多个基类中发现方法 | 197号 |
| 10.62 | 覆盖不明确的方法 | 197号 |
| 10.63 | 重复多重继承 | 198号 |
| 10.64 | C++ 中的共享继承 | 199号 |
| 10.65 | Eiffel 中的复制继承 | 199号 |
| 10.66 | 使用复制继承 | C 200 |
| 10.67 | 使用共享继承覆盖方法 | C 201 |
| 10.68 | 共享继承的实现 | C 201 |
| 重新审视面向对象编程 | ||
| 10.69 | Smalltalk 中的消息操作 | 204号 |
| 10.70 | Mixfix 消息 | 204号 |
| 10.71 | 选择为ifTrue: ifFalse:消息 | C 205 |
| 10.72 | 迭代消息 | C 205 |
| 10.73 | 块作为闭包 | C 206 |
| 10.74 | 消息的逻辑循环 | C 206 |
| 10.75 | 定义控制抽象 | C 206 |
| 10.76 | Smalltalk 中的递归 | 207号 |
| 第 11 章:函数式语言 | ||
| 历史起源 | ||
| 函数式编程概念 | ||
| 一点计划 | ||
| 11.1 | 读取-求值-打印循环 | 539 |
| 11.2 | 括号的意义 | 540 |
| 11.3 | 引用 | 540 |
| 11.4 | 动态类型 | 540 |
| 11.5 | 类型谓词 | 541 |
| 11.6 | 符号的自由语法 | 541 |
| 11.7 | lambda表达式 | 541 |
| 11.8 | 功能评估 | 542 |
| 11.9 | if表达式 | 542 |
| 11.10 | 使用let嵌套作用域 | 542 |
| 11.11 | 使用define进行全局绑定 | 543 |
| 11.12 | 基本列表操作 | 543 |
| 11.13 | 列表搜索功能 | 544 |
| 11.14 | 搜索关联列表 | 545 |
| 11.15 | 多路条件表达式 | 545 |
| 11.16 | 任务 | 545 |
| 11.17 | 测序 | 545 |
| 11.18 | 迭代 | 546 |
| 11.19 | 将数据评估为代码 | 547 |
| 11.20 | 在 Scheme 中模拟 DFA | 548 |
| 一些 OCaml | ||
| 11.21 | 与解释器交互 | 551 |
| 11.22 | 函数调用语法 | 551 |
| 11.23 | 函数值 | 552 |
| 11.24 | 单位类型 | 552 |
| 11.25 | “物理”与“结构”比较 | 553 |
| 11.26 | 最外层声明 | 554 |
| 11.27 | 嵌套声明 | 555 |
| 11.28 | 递归嵌套函数(重复示例 7.38) | 555 |
| 11.29 | 多态列表运算符 | 555 |
| 11.30 | 列表符号 | 556 |
| 11.31 | 数组表示法 | 556 |
| 11.32 | 字符串作为字符数组 | 557 |
| 11.33 | 元组表示法 | 557 |
| 11.34 | 记录符号 | 557 |
| 11.35 | 可变字段 | 558 |
| 11.36 | 参考 | 558 |
| 11.37 | 枚举类型的变体 | 558 |
| 11.38 | 变体作为联合体 | 558 |
| 11.39 | 递归变体 | 559 |
| 11.40 | 参数模式匹配 | 559 |
| 11.41 | 局部声明中的模式匹配 | 560 |
| 11.42 | match 构造 | 560 |
| 11.43 | 守卫 | 561 |
| 11.44 | as关键字 | 561 |
| 11.45 | function关键字 | 561 |
| 11.46 | 运行时模式匹配 | 562 |
| 11.47 | 图案覆盖范围 | 562 |
| 11.48 | 根据函数返回的元组进行模式匹配 | 562 |
| 11.49 | 没有else的if | 563 |
| 11.50 | OCaml 中的插入排序 | 563 |
| 11.51 | 一个简单的异常 | 564 |
| 11.52 | 带参数的异常 | 564 |
| 11.53 | 捕获异常 | 564 |
| 11.54 | 在 OCaml 中模拟 DFA | 565 |
| 重新审视评估顺序 | ||
| 11.55 | 应用和正常顺序评估 | 567 |
| 11.56 | 正常顺序避免不必要的工作 | 568 |
| 11.57 | 避免使用惰性求值 | 570 |
| 11.58 | 基于流的程序执行 | 571 |
| 11.59 | 使用流进行交互式 I/O | 572 |
| 11.60 | Haskell 中的伪随机数 | 572 |
| 11.61 | IO monad的状态 | 574 |
| 11.62 | 动作的功能组合 | 574 |
| 11.63 | 流和 I/O monad | 575 |
| 高阶函数 | ||
| 11.64 | Scheme 中的map函数 | 576 |
| 11.65 | Scheme 中的折叠(缩减) | 576 |
| 11.66 | OCaml 中的折叠 | 576 |
| 11.67 | 组合高阶函数 | 576 |
| 11.68 | 柯里化的部分应用 | 577 |
| 11.69 | 通用柯里化函数 | 577 |
| 11.70 | 元组作为 OCaml 函数参数 | 578 |
| 11.71 | 单例参数上的可选括号 | 578 |
| 11.72 | OCaml 中的简单柯里化函数 | 578 |
| 11.73 | 柯里化的简写形式 | 579 |
| 11.74 | 在 OCaml 中构建fold_left | 579 |
| 11.75 | OCaml 与 Scheme 中的柯里化 | 580 |
| 11.76 | 声明性(非构造性)函数定义 | 580 |
| 11.77 | 函数作为映射 | 212号 |
| 11.78 | 函数作为集合 | 212号 |
| 11.79 | 作为幂集元素的函数 | 213号 |
| 11.80 | 功能空间 | 213号 |
| 11.81 | 高阶函数作为集合 | 213号 |
| 11.82 | 将函数柯里化为集合 | 213号 |
| 11.83 | 并置作为功能应用 | 214号 |
| 11.84 | Lambda 演算语法 | 214号 |
| 11.85 | 使用 λ 绑定参数 | 214号 |
| 11.86 | 自由变量 | 215 号 |
| 11.87 | 命名函数以供将来参考 | 215 号 |
| 11.88 | 评估规则 | 215 号 |
| 11.89 | 减少算术运算的增量 | 215 号 |
| 11.90 | 埃塔还原 | 216号 |
| 11.91 | 简化为最简形式 | 216号 |
| 11.92 | 非终止应用阶减少 | 217号 |
| 11.93 | 布尔值和条件 | 218号 |
| 11.94 | 递归的 Beta 抽象 | 218号 |
| 11.95 | 定点组合器Y | 218号 |
| 11.96 | Lambda 演算列表运算符 | 219号 |
| 11.97 | 列出操作员身份 | 219号 |
| 11.98 | Lambda 表达式的嵌套 | 221号 |
| 11.99 | 配对参数和柯里化 | 221号 |
| 函数式编程概览 | ||
| 第 12 章:逻辑语言 | ||
| 逻辑编程概念 | ||
| 12.1 | 霍恩条款 | 592 |
| 12.2 | 解决 | 592 |
| 12.3 | 统一 | 592 |
| 序言 | ||
| 12.4 | 原子、变量、范围和类型 | 593 |
| 12.5 | 结构和谓词 | 593 |
| 12.6 | 事实和规则 | 593 |
| 12.7 | 查询 | 594 |
| 12.8 | Prolog 中的解析 | 595 |
| 12.9 | Prolog 和 ML 中的统一 | 595 |
| 12.10 | 平等与统一 | 595 |
| 12.11 | 无需实例化 | 596 |
| 12.12 | Prolog 中的列表符号 | 596 |
| 12.13 | 函数、谓词和双向规则 | 597 |
| 12.14 | 算术和 is 谓词 | 597 |
| 12.15 | 搜索树探索 | 598 |
| 12.16 | 回溯和实例化 | 599 |
| 12.17 | 规则评估顺序 | 600 |
| 12.18 | 无限回归 | 600 |
| 12.19 | Prolog 中的井字游戏 | 600 |
| 12.20 | 切工 | 604 |
| 12.21 | \+ 及其实现 | 605 |
| 12.22 | 使用 cut 修剪不需要的答案 | 605 |
| 12.23 | 使用剪切进行选择 | 605 |
| 12.24 | 循环失败 | 605 |
| 12.25 | 使用无界生成器进行循环 | 606 |
| 12.26 | 使用get输入字符 | 607 |
| 12.27 | Prolog 程序作为数据 | 607 |
| 12.28 | 修改 Prolog 数据库 | 608 |
| 12.29 | 井字游戏(完整游戏) | 608 |
| 12.30 | 函子谓词 | 608 |
| 12.31 | 在运行时创建术语 | 610 |
| 12.32 | 追求动态目标 | 611 |
| 12.33 | 自定义数据库浏览 | 611 |
| 12.34 | 谓词作为数学对象 | 612 |
| 12.39 | 命题 | 226号 |
| 12.40 | 不同的表达方式 | 226号 |
| 12.41 | 转换为子句形式 | 227号 |
| 12.42 | 转换为 Prolog | 228号 |
| 12.43 | 分离左侧 | 228号 |
| 12.44 | 左侧为空 | 229号 |
| 12.45 | 定理证明是寻找矛盾 | 229号 |
| 12.46 | 斯科勒姆常数 | 230 号 |
| 12.47 | 斯科伦函数 | 230 号 |
| 12.48 | 斯科勒姆化的局限性 | 230 号 |
| 逻辑编程透视 | ||
| 12.35 | 排序速度非常慢 | 613 |
| 12.36 | Prolog中的快速排序 | 614 |
| 12.37 | 否定即失败 | 615 |
| 12.38 | 否定和实例 | 616 |
| 第 13 章:并发 | ||
| 背景和动机 | ||
| 13.1 | C# 中的独立任务 | 626 |
| 13.2 | 简单的竞争条件 | 626 |
| 13.3 | 多线程网络浏览器 | 627 |
| 13.4 | 调度循环网络浏览器 | 628 |
| 13.5 | 缓存一致性问题 | 632 |
| 并发编程基础 | ||
| 13.6 | co-gin 的一般形式 | 638 |
| 13.7 | OpenMP 中的共同开始 | 639 |
| 13.8 | OpenMP 中的并行循环 | 639 |
| 13.9 | C# 中的并行循环 | 639 |
| 13.10 | Fortran 95 中的Forall | 640 |
| 13.11 | OpenMP 的减少 | 641 |
| 13.12 | Ada 中的精细任务 | 641 |
| 13.13 | 共同开始与分叉/加入 | 642 |
| 13.14 | Ada 中的任务类型 | 642 |
| 13.15 | Java 2 中的线程创建 | 643 |
| 13.16 | 在 C# 中创建线程 | 644 |
| 13.17 | Java 5 中的线程池 | 645 |
| 13.18 | 在 Cilk 中生成并同步 | 645 |
| 13.19 | 使用 fork/join 建模子程序 | 646 |
| 13.20 | 进程上的多路复用线程 | 647 |
| 13.21 | 单处理器上的协作多线程 | 648 |
| 13.22 | 抢占式多线程中的竞争条件 | 650 |
| 13.23 | 上下文切换期间禁用信号 | 651 |
| 实现同步 | ||
| 13.24 | 基本 test_and_set 锁 | 654 |
| 13.25 | 測試-測試-設置 | 654 |
| 13.26 | 有限元分析中的障碍 | 655 |
| 13.27 | “逆向思维”障碍 | 656 |
| 13.28 | Java 7 移相器 | 656 |
| 13.29 | 使用CAS进行原子更新 | 657 |
| 13.30 | M&S 队列 | 658 |
| 13.31 | 写缓冲区和一致性 | 659 |
| 13.32 | 分布式一致性 | 661 |
| 13.33 | 使用volatile避免数据竞争 | 662 |
| 13.34 | 在进程上调度线程 | 663 |
| 13.35 | 线程调度中的竞争条件 | 664 |
| 13.36 | “旋转然后让出”锁 | 665 |
| 13.37 | 缓冲区受限问题 | 666 |
| 13.38 | 信号量的实现 | 667 |
| 13.39 | 带信号量的有界缓冲区 | 668 |
| 语言级结构 | ||
| 13.40 | 有界缓冲区监视器 | 670 |
| 13.41 | 如何等待信号(提示或绝对) | 671 |
| 13.42 | 原始 CCR 语法 | 674 |
| 13.43 | Java 中的synchronized语句 | 676 |
| 13.44 | 在 Java 中以提示形式通知 | 676 |
| 13.45 | Java 5 中的锁定变量 | 677 |
| 13.46 | Java 5 中的多重条件 | 678 |
| 13.47 | 一个简单的原子块 | 680 |
| 13.48 | 有交易的有限缓冲区 | 680 |
| 13.49 | 原子块的翻译 | 681 |
| 13.50 | Multilisp 中的未来构造 | 684 |
| 13.51 | C# 中的 Future | 684 |
| 13.52 | C++11 中的 Future | 685 |
| 13.53 | 命名进程、端口和条目 | 235 号 |
| 13.54 | Ada 中的入口调用 | 235 号 |
| 13.55 | Go 中的 Channels | 236号 |
| 13.56 | Go 中的远程调用 | 237号 |
| 13.57 | Java 中的数据报消息 | 238号 |
| 13.58 | Java 中基于连接的消息 | 238号 |
| 13.59 | 发送语义的三个主要选项 | C 240 |
| 13.60 | 缓冲依赖型死锁 | 241号 |
| 13.61 | 致谢 | 242号 |
| 13.62 | Ada 83 中的有界缓冲区 | 245 号 |
| 13.63 | 超时和分布式终止 | 246号 |
| 13.64 | Go 中的有界缓冲区 | 246号 |
| 13.65 | Erlang 中的有界缓冲区 | 247号 |
| 13.66 | 在 Erlang 中查看消息 | 247号 |
| 13.67 | RPC 服务器系统 | 251号 |
| 第 14 章:脚本语言 | ||
| 什么是脚本语言? | ||
| 14.1 | 传统语言和脚本语言中的简单程序 | 702 |
| 14.2 | Perl 中的强制转换 | 703 |
| 问题领域 | ||
| 14.3 | “通配符”和“通配符” | 706 |
| 14.4 | shell 中的For循环 | 706 |
| 14.5 | 一行一个循环 | 706 |
| 14.6 | shell 中的条件测试 | 707 |
| 14.7 | 管道 | 708 |
| 14.8 | 输出重定向 | 708 |
| 14.9 | stderr和stdout的重定向 | 708 |
| 14.10 | Heredocs(内联输入) | 709 |
| 14.11 | 文件名中存在空格问题 | 709 |
| 14.12 | 单引号和双引号 | 709 |
| 14.13 | 子壳层 | 709 |
| 14.14 | shell 中括号引用的块 | 710 |
| 14.15 | 基于模式的列表生成 | 710 |
| 14.16 | 用户定义的 shell 函数 | 710 |
| 14.17 | 脚本文件中的# !约定 | 711 |
| 14.18 | 使用sed提取 HTML 标头 | 713 |
| 14.19 | sed中的单行脚本 | 713 |
| 14.20 | 使用awk提取 HTML 标头 | 714 |
| 14.21 | awk中的字段 | 715 |
| 14.22 | 在awk中将标题大写 | 715 |
| 14.23 | 使用 Perl 提取 HTML 标头 | 716 |
| 14.24 | Perl 中的“强制退出”脚本 | 718 |
| 14.25 | Python 中的“强制退出”脚本 | 720 |
| 14.26 | Ruby 中的方法调用语法 | 722 |
| 14.27 | Ruby 中的“强制退出”脚本 | 722 |
| 14.28 | 使用 Emacs Lisp 对行进行编号 | 725 |
| 编写万维网脚本 | ||
| 14.29 | 使用 CGI 脚本进行远程监控 | 728 |
| 14.30 | 带有 CGI 脚本的 Adder Web 表单 | 728 |
| 14.31 | 使用 PHP 脚本进行远程监控 | 731 |
| 14.32 | 碎片化的 PHP 脚本 | 731 |
| 14.33 | 带有 PHP 脚本的 Adder Web 表单 | 732 |
| 14.34 | 自发布加法器网络表单 | 732 |
| 14.35 | 使用 JavaScript 编写的 Adder Web 表单 | 734 |
| 14.36 | 在网页中嵌入小程序 | 735 |
| 14.81 | HTML 中的内容与呈现 | 258号 |
| 14.82 | 格式正确的 XHTML | 259号 |
| 14.83 | 使用 XHTML 显示喜爱的引言 | 261号 |
| 14.84 | XHTML 元素的 XPath 名称 | 262号 |
| 14.85 | 使用 XSLT 创建参考列表 | 262号 |
| 创新功能 | ||
| 14.37 | Python 中的作用域规则 | 740 |
| 14.38 | R 中的超级分配 | 740 |
| 14.39 | Perl 中的静态和动态作用域 | 741 |
| 14.40 | 在 Perl 中访问全局变量 | 742 |
| 14.41 | POSIX RE 中的基本操作 | 744 |
| 14.42 | POSIX RE 中的额外量词 | 744 |
| 14.43 | 零长度断言 | 744 |
| 14.44 | 字符类 | 744 |
| 14.45 | 点 (.) 字符 | 745 |
| 14.46 | 字符类中的否定和引用 | 745 |
| 14.47 | 预定义 POSIX 字符类 | 745 |
| 14.48 | Perl 中的 RE 匹配 | 745 |
| 14.49 | 在 Perl 中否定匹配 | 746 |
| 14.50 | Perl 中的 RE 替换 | 746 |
| 14.51 | RE 匹配中的尾随修饰符 | 746 |
| 14.52 | 贪婪和最小匹配 | 748 |
| 14.53 | HTML 标头的最小匹配 | 748 |
| 14.54 | 扩展 RE 中的变量插值 | 748 |
| 14.55 | 扩展 RE 中的变量捕获 | 749 |
| 14.56 | 扩展 RE 中的反向引用 | 750 |
| 14.57 | 解析浮点文字 | 750 |
| 14.58 | 隐式捕获前缀、匹配和后缀 | 750 |
| 14.59 | Ruby 和 Perl 中的强制转换 | 751 |
| 14.60 | Perl 中的强制转换和上下文 | 751 |
| 14.61 | Ruby 中的显式转换 | 752 |
| 14.62 | Perl 数组 | 753 |
| 14.63 | Perl 哈希 | 753 |
| 14.64 | Python 和 Ruby 中的数组和哈希 | 754 |
| 14.65 | Ruby 中的数组访问方法 | 755 |
| 14.66 | Python 中的元组 | 755 |
| 14.67 | Python 中的集合 | 755 |
| 14.68 | PHP、Tcl 和 JavaScript 中的混合类型 | 755 |
| 14.69 | Python 和其他语言中的多维数组 | 755 |
| 14.70 | Perl 中的标量和列表上下文 | 756 |
| 14.71 | 使用 wantarray 确定调用上下文 | 757 |
| 14.72 | Perl 中的一个简单类 | 757 |
| 14.73 | 在 Perl 中调用方法 | 758 |
| 14.74 | Perl 中的继承 | 759 |
| 14.75 | 通过use base继承 | 759 |
| 14.76 | JavaScript 中的原型 | 760 |
| 14.77 | 在 JavaScript 中重写实例方法 | 761 |
| 14.78 | JavaScript 中的继承 | 761 |
| 14.79 | Python 和 Ruby 中的构造函数 | 762 |
| 14.80 | 在 Python 和 Ruby 中命名类成员 | 762 |
| 第 15 章:构建可运行程序 | ||
| 后端编译器结构 | ||
| 15.1 | 编译阶段 | 776 |
| 15.2 | GCD 程序抽象语法树(重演) | 776 |
| 中级形式 | ||
| 15.3 | 图 15.1中的中间形式 | 781 |
| 15.19 | GIMPLE 中的 GCD 程序 | 273号 |
| 15.20 | RTL insn序列 | 276号 |
| 15.4 | 计算海伦公式 | 783 |
| 代码生成 | ||
| 15.5 | 更简单的编译器结构 | 784 |
| 15.6 | 用于代码生成的属性语法 | 785 |
| 15.7 | 基于堆栈的寄存器分配 | 787 |
| 15.8 | GCD 程序目标代码 | 788 |
| 地址空间组织 | ||
| 15.9 | Linux 地址空间布局 | 792 |
| 集会 | ||
| 15.10 | 汇编作为最终的编译过程 | 792 |
| 15.11 | 直接生成目标代码 | 794 |
| 15.12 | 压缩nops | 794 |
| 15.13 | 相对分支和绝对分支 | 794 |
| 15.14 | 伪指令 | 795 |
| 15.15 | 汇编程序指令 | 795 |
| 15.16 | 目标文件中地址的编码 | 796 |
| 链接 | ||
| 15.17 | 静态链接 | 798 |
| 15.18 | 对标头进行校验以确保一致性 | 799 |
| 15.21 | x86/Linux 下的 PIC | 280 号 |
| 15.22 | x86 上的 PC 相对寻址 | 282号 |
| 15.23 | x86 上的 Linux 中的动态链接 | 282号 |
| 第 16 章:运行时程序管理 | ||
| 16.1 | CLI 作为运行时系统和虚拟机 | 807 |
| 虚拟机 | ||
| 16.2 | “Hello, world” 的常量 | 813 |
| 16.3 | 列表插入操作的字节码 | 818 |
| 16.39 | CLI 和 JVM 中的泛型 | 291号 |
| 16.40 | 列表插入操作的 CIL | 292号 |
| 机器代码的后期绑定 | ||
| 16.4 | 内联什么时候是安全的? | 824 |
| 16.5 | 推测优化 | 825 |
| 16.6 | CLR 中的动态编译 | 826 |
| 16.7 | CMU Common Lisp 中的动态编译 | 827 |
| 16.8 | Perl 的编译 | 827 |
| 16.9 | Mac 68K 模拟器 | 829 |
| 16.10 | Transmeta Crusoe 处理器 | 829 |
| 16.11 | 静态二进制翻译 | 830 |
| 16.12 | 动态二进制翻译 | 830 |
| 16.13 | 混合口译和笔译 | 830 |
| 16.14 | 透明动态翻译 | 831 |
| 16.15 | 翻译和虚拟化 | 831 |
| 16.16 | Dynamo 动态优化器 | 831 |
| 16.17 | ATOM 二进制重写器 | 833 |
| 检查/自省 | ||
| 16.18 | 查找引用变量的具体类型 | 837 |
| 16.19 | 反射不应该做什么 | 838 |
| 16.20 | Java 类命名约定 | 838 |
| 16.21 | 获取特定类的信息 | 839 |
| 16.22 | 列出 Java 类的方法 | 839 |
| 16.23 | 使用反射调用方法 | 840 |
| 16.24 | Ruby 中的反射功能 | 841 |
| 16.25 | Java 中的用户定义注释 | 842 |
| 16.26 | C# 中的用户定义注释 | 842 |
| 16.27 | javadoc | 842 |
| 16.28 | 组件间通信 | 843 |
| 16.29 | LINQ 的属性 | 843 |
| 16.30 | Java 建模语言 | 844 |
| 16.31 | Java 注释处理器 | 845 |
| 16.32 | 设置断点 | 847 |
| 16.33 | 硬件断点 | 847 |
| 16.34 | 设置观察点 | 847 |
| 16.35 | 统计抽样 | 848 |
| 16.36 | 调用图分析 | 848 |
| 16.37 | 查找低 IPC 的基本块 | 849 |
| 16.38 | Haswell 性能计数器 | 849 |
| 第 17 章:代码改进 | ||
| 17.1 | 代码改进阶段 | 299号 |
| 17.2 | 消除冗余加载和存储 | C 301 |
| 17.3 | 常量折叠 | C 301 |
| 17.4 | 持续传播 | C 301 |
| 17.5 | 公共子表达式消除 | C 302 |
| 17.6 | 复制传播 | C 302 |
| 17.7 | 强度降低 | C 302 |
| 17.8 | 消除无用指令 | C 303 |
| 17.9 | 指令集的利用 | C 303 |
| 17.10 | 组合子程序 | C 305 |
| 17.11 | 语法树和简单控制流图 | C 305 |
| 17.12 | 局部冗余消除结果 | C 310 |
| 17.13 | 转换为 SSA 表格 | 313 号 |
| 17.14 | 全局值编号 | 313 号 |
| 17.15 | 可用表达式的数据流方程 | 317 号 |
| 17.16 | 可用表达式的固定点 | 317 号 |
| 17.17 | 全局公共子表达式消除的结果 | 318 号 |
| 17.18 | 边分裂变换 | 319 号 |
| 17.19 | 活动变量的数据流方程 | 321 号 |
| 17.20 | 活动变量的固定点 | 321 号 |
| 17.21 | 用于达到定义的数据流方程 | 324 号 |
| 17.22 | 提升循环不变量的结果 | 325 号 |
| 17.23 | 感应可变强度降低 | 325 号 |
| 17.24 | 归纳变量消除 | 326 号 |
| 17.25 | 诱导变量优化结果 | 326 号 |
| 17.26 | 剩余管道延迟 | 329号 |
| 17.27 | 价值依赖 DAG | 329号 |
| 17.28 | 指令调度结果 | 331 号 |
| 17.29 | 循环展开的结果 | 332 号 |
| 17.30 | 软件流水线的结果 | 333 |
| 17.31 | 环路立交 | 337 号 |
| 17.32 | 循环平铺(阻塞) | 337 号 |
| 17.33 | 循环分布 | 339 号 |
| 17.34 | 循环融合 | 339 号 |
| 17.35 | 获得完美的循环嵌套 | 339 号 |
| 17.36 | 循环依赖 | 340 号 |
| 17.37 | 循环反转和交换 | 341 号 |
| 17.38 | 循环倾斜 | 341 号 |
| 17.39 | 粗粒度并行化 | 343 号 |
| 17.40 | 露天开采 | 343 号 |
| 17.41 | 虚拟寄存器的有效范围 | 344 号 |
| 17.42 | 登记着色 | 344 号 |
| 17.43 | 优化组合子程序 | 346 号 |
[亚洲电话+ 96]Amza Cristiana、Cox Alan L.、Dwarkadas Sandhya、Keleher Pete、Lu Honghui、Rajamony Ramakrishnan、Yu Weimin、Zwaenepoel Willy。TreadMarks:工作站网络上的共享内存计算。IEEE计算机。1996;29(2):2 月 18-28 日。
[ACD+96] Amza Cristiana, Cox Alan L., Dwarkadas Sandhya, Keleher Pete, Lu Honghui, Rajamony Ramakrishnan, Yu Weimin, Zwaenepoel Willy. TreadMarks: Shared memory computing on networks of workstations. IEEE Computer. 1996;29(2):18–28 February.
[Ado90]Adobe Systems, Inc. PostScript 语言参考手册。第二版,马萨诸塞州雷丁:Addison-Wesley;1990 年。
[Ado90] Adobe Systems, Inc. PostScript Language Reference Manual. second edition Reading, MA: Addison-Wesley; 1990.
[AF84]Apt Krzysztof R.,Francez Nissim。CSP 的分布式终止约定建模。ACM编程语言和系统事务。1984;6(3):7 月 370-379。
[AF84] Apt Krzysztof R., Francez Nissim. Modeling the distributed termination convention of CSP. ACM Transactions on Programming Languages and Systems. 1984;6(3):370–379 July.
[AFG + 05]Arnold Matthew、Fink Stephen J.、Grove David、Hind Michael、Sweeney Peter F. 虚拟机自适应优化调查。IEEE论文集。2005;93(2):2 月 449-466 日。
[AFG+05] Arnold Matthew, Fink Stephen J., Grove David, Hind Michael, Sweeney Peter F. A survey ofadaptive optimization in virtual machines. Proceedings of the IEEE. 2005;93(2):449–466 February.
[AG96]Adve Sarita V.,Gharachorloo Kourosh。共享内存一致性模型:教程。IEEE计算机。1996;29(12):66-76 十二月。
[AG96] Adve Sarita V., Gharachorloo Kourosh. Shared memory consistency models: A tutorial. IEEE Computer. 1996;29(12):66–76 December.
[AG05]Abrahams David、Gurtovoy Aleksey。C ++ 模板元编程:来自 Boost 及其他语言的概念、工具和技术。马萨诸塞州波士顿:Addison-Wesley;2005 年。
[AG05] Abrahams David, Gurtovoy Aleksey. C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond. Boston, MA: Addison-Wesley; 2005.
[AG06]Arnold Ken、Gosling James。《Java 编程语言》。第四版 Addison-Wesley Professional;2006 年。
[AG06] Arnold Ken, Gosling James. The Java Programming Language. fourth edition Addison-Wesley Professional; 2006.
[AH95]Agesen Ole,Holzle Urs。类型反馈与具体类型推断:面向对象语言优化技术的比较。收录于:第十届 ACM SIGPLAN 面向对象编程系统、语言和应用程序会议论文集;1995:91–107 德克萨斯州奥斯汀,十月。
[AH95] Agesen Ole, Holzle Urs. Type feedback v. concrete type inference: A comparison of optimization techniques for object-oriented languages. In: Proceedings of the Tenth ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages, and Applications; 1995:91–107 Austin, TX, October.
[AK02]Allen Randy、Kennedy Ken。《针对现代架构优化编译器:基于依赖性的方法》。加利福尼亚州旧金山:Morgan Kaufmann;2002 年。
[AK02] Allen Randy, Kennedy Ken. Optimizing Compilers for Modern Architectures: A Dependence-Based Approach. San Francisco, CA: Morgan Kaufmann; 2002.
[AKW88]Aho Alfred V.、Kernighan Brian W. 和 Weinberger Peter J. 《AWK 编程语言》。马萨诸塞州雷丁:Addison-Wesley;1988 年。
[AKW88] Aho Alfred V., Kernighan Brian W., Weinberger Peter J. The AWK Programming Language. Reading, MA: Addison-Wesley; 1988.
[全部69]Allen Frances E. 程序优化。《自动编程年度评论》。1969;5:239-307。
[All69] Allen Frances E. Program optimization. Annual Review in Automatic Programming. 1969;5:239–307.
[ALSU07]Aho Alfred V.、Lam Monica S.、Sethi Ravi 和 Ullman Jeffrey D.编译器:原理、技术和工具。第二版,波士顿,马萨诸塞州:Addison-Wesley;2007 年。
[ALSU07] Aho Alfred V., Lam Monica S., Sethi Ravi, Ullman Jeffrey D. Compilers: Principles, Techniques, and Tools. second edition Boston, MA: Addison-Wesley; 2007.
[Ame78]美国国家标准协会,纽约,纽约州。编程语言最小 BASIC。1978 ANSI X3.60–1978。
[Ame78] American National Standards Institute, New York, NY. Programming Language Minimal BASIC. 1978 ANSI X3.60–1978.
[Ame83]美国国家标准协会,纽约,纽约州。Ada编程语言参考手册。1983年 1 月 ANSI/MIL 1815 A–1983。
[Ame83] American National Standards Institute, New York, NY. Reference Manual for the Ada Programming Language. 1983 January ANSI/MIL 1815 A–1983.
[Ame90]美国国家标准协会,纽约州纽约市。编程语言 C。1990 ANSI/ISO 9899–1990(ANSI X3.159–1989 的修订和重新指定)。
[Ame90] American National Standards Institute, New York, NY. Programming Language C. 1990 ANSI/ISO 9899–1990 (revision and redesignation of ANSI X3.159–1989).
[Ame96a]美国国家标准协会,纽约,纽约州。信息技术 - 编程语言 REXX。1996 ANSI INCITS 274-1996/AMD1-2000 (R2001)。
[Ame96a] American National Standards Institute, New York, NY. Information Technology—Programming Language REXX. 1996 ANSI INCITS 274-1996/AMD1-2000 (R2001).
[Ame96b]美国国家标准协会,纽约,纽约州。编程语言 - Common Lisp。1996年。ANSI X3.226:1994。可从lispworks.com/documentation/common-lisp.html获取。
[Ame96b] American National Standards Institute, New York, NY. Programming Language—Common Lisp. 1996. ANSI X3.226:1994. Available at lispworks.com/documentation/common-lisp.html.
[AO93]Andrews Gregory R.,Olsson Ronald A. SR 编程语言:实践中的并发性。加利福尼亚州雷德伍德城:Benjamin/Cummings;1993 年。
[AO93] Andrews Gregory R., Olsson Ronald A. The SR Programming Language: Concurrency in Practice. Redwood City, CA: Benjamin/Cummings; 1993.
[应用91]Appel Andrew W.《Compiling with Continuations》。英国剑桥:剑桥大学出版社;1991 年。
[App91] Appel Andrew W. Compiling with Continuations. Cambridge, England: Cambridge University Press; 1991.
[应用97]Appel Andrew W.现代编译器实现。英国剑桥:剑桥大学出版社;1997 文本提供 ML、Java 和 C 版本。C 版本由 Maia Ginsburg 专门编写;Java 版本(第二版,2002 年)由 Jens Palsberg 专门编写。
[App97] Appel Andrew W. Modern Compiler Implementation. Cambridge, England: Cambridge University Press; 1997 Text available in ML, Java, and C versions. C version specialized by Maia Ginsburg; Java version (second edition, 2002) specialized by Jens Palsberg.
[Arm07]阿姆斯特朗·乔。《Erlang 到底为什么这么受关注?》《实用程序员》。2007年。可访问pragprog.com/articles/erlang.html。
[Arm07] Armstrong Joe. What's all this fuss about Erlang? The Pragmatic Programmers. 2007. Available at pragprog.com/articles/erlang.html.
[手臂13]阿姆斯特朗·乔。《编程 Erlang:面向并发世界的软件》。第二版《实用书架》;2013 年。
[Arm13] Armstrong Joe. Programming Erlang: Software for a Concuirrent World. second edition Pragmatic Bookshelf; 2013.
[第 83 条]Andrews Gregory R.,Schneider Fred B. 并发编程的概念和符号。ACM计算调查。1983;15(1):3 月 3-43 日。
[AS83] Andrews Gregory R., Schneider Fred B. Concepts and notations for concurrent programming. ACM Computing Surveys. 1983;15(1):3–43 March.
[AS96]Abelson Harold,Sussman Gerald Jay。《计算机程序的结构和解释》。第二版,马萨诸塞州剑桥:麻省理工学院出版社;1996 年。与 Julie Sussman 合著。全文和补充资源可在mitpress.mit.edu/sicp/上找到。
[AS96] Abelson Harold, Sussman Gerald Jay. Structure and Interpretation of Computer Programs. second edition Cambridge, MA: MIT Press; 1996. With Julie Sussman. Full text and supplementary resources available at mitpress.mit.edu/sicp/.
[Ass93]计算机协会,纽约,纽约州。在:第二届 ACM SIGPLAN 编程语言历史 (HOPL) 会议论文集,马萨诸塞州剑桥;1993 年 4 月在 ACM SIGPLAN 通知,28(3),1993 年 3 月。
[Ass93] Association for Computing Machinery, New York, NY. In: Proceedings of the Second ACM SIGPLAN History of Programming Languages (HOPL) Conference, Cambridge, MA; 1993 April In ACM SIGPLAN Notices, 28(3), March 1993.
[Ass07]计算机协会,纽约,纽约州。第三届 ACM SIGPLAN 编程语言历史 (HOPL) 会议论文集,加利福尼亚州圣地亚哥;2007 年 6 月。
[Ass07] Association for Computing Machinery, New York, NY. In: Proceedings of the Third ACM SIGPLAN History of Programming Languages (HOPL) Conference, San Diego, CA; 2007 June.
[攻击73]Stella Atkins M. 使用受限编译器在 Algol 60 中实现相互递归。《ACM 通讯》。1973;16(1):47-48。
[Atk73] Stella Atkins M. Mutual recursion in Algol 60 using restricted compilers. Communications of the ACM. 1973;16(1):47–48.
[AU72]Aho Alfred V.、Ullman Jeffrey D. 《解析、翻译和编译理论》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1972 年两卷本。
[AU72] Aho Alfred V., Ullman Jeffrey D. The Theory of Parsing, Translation and Compiling. Englewood Cliffs, NJ: Prentice-Hall; 1972 Two-volume set.
[AWZ88]Alpern Bowen、Wegman Mark N.、Kenneth Zadeck F.检测程序中变量的相等性。见:第十五届 ACM 编程语言原理研讨会会议记录;1988 年 1 月:1-11 加利福尼亚州圣地亚哥。
[AWZ88] Alpern Bowen, Wegman Mark N., Kenneth Zadeck F. Detecting equality of variables in programs. In: Conference Record of the Fifteenth ACM Symposium on Principles of Programming Languages; 1988:1–11 San Diego, CA, January.
[Ayc03]Aycock John。即时生产简史。ACM计算调查。2003;35(2):6 月 97-113。
[Ayc03] Aycock John. A brief history of just-in-time. ACM Computing Surveys. 2003;35(2):97–113 June.
[BA08]Boehm Hans-J.、Adve Sarita V. C++ 并发内存模型基础。收录于:SIGPLAN 2008 编程语言设计和实现会议论文集;2008:68–78 亚利桑那州图森,六月。
[BA08] Boehm Hans-J., Adve Sarita V. Foundations of the C++ concurrency memory model. In: Proceedings of the SIGPLAN 2008 Conference on Programming Language Design and Implementation; 2008:68–78 Tucson, ZA, June.
[Bac78]Backus John W. 编程能从冯·诺依曼风格中解放出来吗?一种函数式风格及其程序代数。ACM通讯。1978;21(8):613–641 八月 1977 年图灵奖演讲。
[Bac78] Backus John W. Can programming be liberated from the von Neumann style? A functional style and its algebra of programs. Communications of the ACM. 1978;21(8):613–641 August The 1977 Turing Award lecture.
[糟糕+ 09]Bocchino Robert L. Jr.、Adve Vikram S.、Dig Danny、Adve Sarita、Heumann Stephen、Komuravelli Rakesh、Overbey Jeffrey、Simmons Patrick、Sung Hyojin、Vakilian Mohsen。确定性并行 Java 的类型和效果系统。收录于:第 24 届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集,佛罗里达州奥兰多;2009 年 10 月。
[BAD+09] Bocchino Robert L. Jr., Adve Vikram S., Dig Danny, Adve Sarita, Heumann Stephen, Komuravelli Rakesh, Overbey Jeffrey, Simmons Patrick, Sung Hyojin, Vakilian Mohsen. A type and effect system for deterministic parallel Java. In: Proceedings of the Twenty-Fourth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, Orlando, FL; 2009 October.
[袋子89]Bagrodia Rajive。CSP 中的异步进程同步。ACM编程语言和系统事务。1989;11(4):10 月 585-597 日。
[Bag89] Bagrodia Rajive. Synchronizatiom of asynchronous processes in CSP. ACM Transactions on Programming Languages and Systems. 1989;11(4):585–597 October.
[球90]Bershad Brian N.、Anderson Thomas E.、Lazowska Edward D.、Levy Henry M. 轻量级远程过程调用。ACM计算机系统学报。1990;8(1):2 月 37-55 日。
[BALL90] Bershad Brian N., Anderson Thomas E., Lazowska Edward D., Levy Henry M. Lightweight remote procedure call. ACM Transactions on Computer Systems. 1990;8(1):37–55 February.
[Ban97]Banerjee Utpal。《依赖性分析》,《重构编译器的循环转换》第 3 卷。马萨诸塞州波士顿:Kluwer Academic Publishers;1997 年。
[Ban97] Banerjee Utpal. Dependence Analysis, volume 3 of Loop Transformations for Restructuring Compilers. Boston, MA: Kluwer Academic Publishers; 1997.
[酒吧84]Barendregt Hendrik Pieter。《Lambda 演算:其语法和语义》,《逻辑与数学基础研究》第 103 卷。修订版荷兰:北荷兰,阿姆斯特丹;1984 年。
[Bar84] Barendregt Hendrik Pieter. The Lambda Calculus: Its Syntax and Semantics, volume 103 of Studies in Logic and the Foundations of Mathematics. revised edition The Netherlands: North-Holland, Amsterdam; 1984.
[BCR04]Bacon David F.、Cheng Perry、Rajan VT垃圾收集统一理论。收录于:第十九届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;2004 年 10 月,加拿大不列颠哥伦比亚省温哥华,50–68 页。
[BCR04] Bacon David F., Cheng Perry, Rajan V.T. A unified theory of garbage collection. In: Proceedings of the Nineteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 2004:50–68 Vancouver, BC, Canada, October.
[BDB00]Bala Vasanth、Duesterwald Evelyn、Banerjia Sanjeev。Dynamo :透明的动态优化系统。收录于:SIGPLAN 2000 编程语言设计和实现会议论文集;2000 年 6 月,加拿大不列颠哥伦比亚省温哥华,1-12。
[BDB00] Bala Vasanth, Duesterwald Evelyn, Banerjia Sanjeev. Dynamo: A transparent dynamic optimization system. In: Proceedings of the SIGPLAN 2000 Conference on Programming Language Design and Implementation; 2000:1–12 Vancouver, BC, Canada, June.
[BDMN73]Birtwistle Graham M.、Dahl Ole-Johan、Myhrhaug Bjorn、Nygaard Kristen。模拟开始。宾夕法尼亚州费城:Auerback Publishers, Inc.; 1973年。
[BDMN73] Birtwistle Graham M., Dahl Ole-Johan, Myhrhaug Bjorn, Nygaard Kristen. SIMULA Begin. Philadelphia, PA: Auerback Publishers, Inc.; 1973.
[BEC97]Beck Leland L.系统软件:系统编程简介。第三版,马萨诸塞州雷丁:Addison-Wesley;1997 年。
[Bec97] Beck Leland L. System Software: An Introduction to Systems Programming. third edition Reading, MA: Addison-Wesley; 1997.
[Bee70]Beech David。PL/I 的结构视图。ACM计算调查。1970;2(1):3 月 33-64 日。
[Bee70] Beech David. A structural view of PL/I. ACM Computing Surveys. 1970;2(1):33–64 March.
[贝尔05]贝拉德·法布里斯。QEMU,一种快速便携的动态翻译器。摘自:USENIX 2005 年度技术会议论文集;2005:41-46 加利福尼亚州阿纳海姆,4 月。
[Bel05] Bellard Fabrice. QEMU, a fast and portable dynamic translator. In: Proceedings of the USENIX 2005 Annual Technical Conference; 2005:41–46 Anaheim, CA, April.
[本00]Bentley John L.编程精粹。第一版 Addison-Wesley Professional;2000 1986 年。
[Ben00] Bentley John L. Programming Pearls. First edition Addison-Wesley Professional; 2000 1986.
[Ber85]Bernstein Robert L. 为 case 语句生成良好的代码。软件——实践与经验。1985;15(10):1021–1024 十月 Sampath Kannan 和 Todd A. Proebsting 的更正出现在第 24 卷第 2 期中。
[Ber85] Bernstein Robert L. Producing good code for the case statement. Software—Practice and Experience. 1985;15(10):1021–1024 October A correction, by Sampath Kannan and Todd A. Proebsting, appears in Volume 24, Number 2.
[BFKM86]Brownston Lee、Farrell Robert、Kant Elaine、Martin Nancy。《OPS5 中的专家系统编程:基于规则的编程简介》。马萨诸塞州雷丁:Addison-Wesley;1986 年。
[BFKM86] Brownston Lee, Farrell Robert, Kant Elaine, Martin Nancy. Programming Expert Systems in OPS5: An Introduction to Rule-Based Programming. Reading, MA: Addison-Wesley; 1986.
[BGS94]Bacon David F.,Graham Susan L.,Sharp Oliver J. 高性能计算的编译器转换。ACM计算调查。1994;26(4):345-420 十二月。
[BGS94] Bacon David F., Graham Susan L., Sharp Oliver J. Compiler transformations for high-performance computing. ACM Computing Surveys. 1994;26(4):345–420 December.
[BHJL07] Andrew Black、Norman Hutchinson、Eric Jul 和 Henry Levy。Emerald 编程语言的发展。载于 HOPL III 论文集 [Ass07],第 11-1–11-51 页。
[BHJL07] Andrew Black, Norman Hutchinson, Eric Jul, and Henry Levy. The development of the Emerald programming language. In HOPL III Proceedings [Ass07], pages 11-1–11-51.
[BHPS61]Bar-Hillel Yehoshua、Perles Micha A.、Shamir Eliahu。简单短语结构语法的形式性质。语音、语言科学和通讯研究的时代文献。 1961;14:143-172。
[BHPS61] Bar-Hillel Yehoshua, Perles Micha A., Shamir Eliahu. On formal properties of simple phrase structure grammars. Zeitschrift feur Phonetik, Sprachwissenschaft und Kommunikationsforschung. 1961;14:143–172.
[BI82]Borning Alan H.,Ingalls Daniel HH Smalltalk-80 中的多重继承。引自:AAAI-82:全国人工智能大会;1982:234–237 宾夕法尼亚州匹兹堡,八月。
[BI82] Borning Alan H., Ingalls Daniel H.H. Multiple inheritance in Smalltalk-80. In: AAAI-82: The National Conference on Artificial Intelligence; 1982:234–237 Pittsburgh, PA, August.
[基本法92]Ball Thomas,Larus James R.最佳程序分析和跟踪。收录于:第十九届 ACM 编程语言原理研讨会会议记录;1992:59-70,新墨西哥州阿尔伯克基,1 月。
[BL92] Ball Thomas, Larus James R. Optimally profiling and tracing programs. In: Conference Record of the Nineteenth ACM Symposium on Principles of Programming Languages; 1992:59–70 Albuquerque, NM, January.
[BM77]Boyer Robert S.,Strother Moore J. 一种快速字符串搜索算法。《ACM 通讯》。1977;20(10):762-772 十月。
[BM77] Boyer Robert S., Strother Moore J. A fast string searching algorithm. Communications of the ACM. 1977;20(10):762–772 October.
[BN84]Birrell Andrew D.,Nelson Bruce J. 实现远程过程调用。ACM计算机系统学报。1984;2(1):2 月 39-59 日。
[BN84] Birrell Andrew D., Nelson Bruce J. Implementing remote procedure calls. ACM Transactions on Computer Systems. 1984;2(1):39–59 February.
[博11]Bryant Randal E、David O'Hallaron。计算机系统:程序员的视角。第二版,波士顿,马萨诸塞州:Prentice-Hall;2011 年。
[BO11] Bryant Randal E, O'Hallaron David. Computer Systems: A Programmer's Perspective. second edition Boston, MA: Prentice-Hall; 2011.
[Boe05]Boehm Hans-J。线程不能作为库实现。在:SIGPLAN 2005 编程语言设计和实现会议论文集;2005:261–268 芝加哥,伊利诺斯州,六月。
[Boe05] Boehm Hans-J. Threads cannot be implemented as a library. In: Proceedings of the SIGPLAN 2005 Conference on Programming Language Design and Implementation; 2005:261–268 Chicago, IL, June.
[BOSW98]Bracha Gilad、Odersky Martin、Stoutamire David、Wadler Philip。让未来比过去更安全:为 Java 编程语言添加泛型。收录于:第十三届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1998:183–200 加拿大不列颠哥伦比亚省温哥华,十月。
[BOSW98] Bracha Gilad, Odersky Martin, Stoutamire David, Wadler Philip. Making the future safe for the past: Adding genericity to the Java programming language. In: Proceedings of the Thirteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1998:183–200 Vancouver, BC, Canada, October.
[Bou78]Bourne Stephen R. UNIX shell 简介。Bell System Technical Journal。1978;57(6,第 2 部分):2797–2822 七月至八月。
[Bou78] Bourne Stephen R. An introduction to the UNIX shell. Bell System Technical Journal. 1978;57(6, Part 2):2797–2822 July–August.
[Bri73]Hansen Per Brinch。《操作系统原理》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1973 年。
[Bri73] Hansen Per Brinch. Operating System Principles. Englewood Cliffs, NJ: Prentice-Hall; 1973.
[Bri75]Hansen Per Brinch。编程语言 Concurrent Pascal。IEEE软件工程学报。1975;SE–1(2):6 月 199–207。
[Bri75] Hansen Per Brinch. The programming language Concurrent Pascal. IEEE Transactions on Software Engineering. 1975;SE–1(2):199–207 June.
[Bri81]Hansen Per Brinch。《爱迪生的设计》。软件——实践与经验。1981;11(4):363–396 年 4 月。
[Bri81] Hansen Per Brinch. The design of Edison. Software—Practice and Experience. 1981;11(4):363–396 April.
[Bro87]Brodie Leo。《开始使用 FORTH:面向初学者和专业人士的 FORTH 语言和操作系统简介》。新泽西州恩格尔伍德克利夫斯,第二版:Prentice-Hall 软件系列。Prentice-Hall;1987 年。
[Bro87] Brodie Leo. Starting FORTH: An Introduction to the FORTH Language and Operating System for Beginners and Professionals. Englewood Cliffs, NJ, second edition: Prentice-Hall Software Series. Prentice-Hall; 1987.
[Bro96]Brockschmidt Kraig。OLE 和 COM 如何解决组件软件设计问题。Microsoft Systems Journal。1996;11(5):5 月 63-82 日。
[Bro96] Brockschmidt Kraig. How OLE and COM solve the problems of component software design. Microsoft Systems Journal. 1996;11(5):63–82 May.
[英国标准时间83]Bobrow Daniel G.,Stefik Mark J. LOOPS 手册。Palo Alto,CA:Xerox Palo Alto 研究中心;1983 年技术报告。
[BS83] Bobrow Daniel G., Stefik Mark J. The LOOPS manual. Palo Alto, CA: Xerox Palo Alto Research Center; 1983 Technical report.
[英国标准时间96]Bacon David F.,Sweeney Peter F. C++ 虚拟函数调用的快速静态分析。收录于:第十一届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1996:324–341 加利福尼亚州圣何塞,十月。
[BS96] Bacon David F., Sweeney Peter F. Fast static analysis of C++ virtual function calls. In: Proceedings of the Eleventh ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1996:324–341 San Jose, CA, October.
[BW88]Boehm Hans-Juergen,Weiser Mark。不合作环境下的垃圾收集。软件——实践与经验。1988;18(9):807–820 九月。
[BW88] Boehm Hans-Juergen, Weiser Mark. Garbage collection in an uncooperative environment. Software—Practice and Experience. 1988;18(9):807–820 September.
[CAC + 81]Chaitin Gregory、Auslander Marc、Chandra Ashok、Cocke John、Hopkins Martin、Markstein Peter。通过着色进行寄存器分配。计算机语言。1981;6(1):47-57。
[CAC+81] Chaitin Gregory, Auslander Marc, Chandra Ashok, Cocke John, Hopkins Martin, Markstein Peter. Register allocation via coloring. Computer Languages. 1981;6(1):47–57.
[蔡82]Cailliau R. 如何避免被 Pascal SCHLONKED。ACM SIGPLAN 通知。1982;17(12):12 月 31-40 日。
[Cai82] Cailliau R. How to avoid getting SCHLONKED by Pascal. ACM SIGPLAN Notices. 1982;17(12):31–40 December.
[Can92]坎·戴维。Fortran 退役?争论再起。《ACM 通讯》。1992;35(8):81-89 八月。
[Can92] Cann David. Retire Fortran? A debate rekindled. Communications of the ACM. 1992;35(8):81–89 August.
[CDW04]Chen Hao、Dean Drew、Wagner David。《对一百万行 C 代码进行模型检查》。《网络和分布式系统安全研讨会论文集》;2004 年 2 月,加利福尼亚州圣地亚哥,171-185。
[CDW04] Chen Hao, Dean Drew, Wagner David. Model checking one million lines of C code. In: Proceedings of the Network and Distributed System Security Symposium; 2004:171–185 San Diego, CA, February.
[Cer89]Ceruzzi Paul。《超越极限——飞行进入计算机时代》。马萨诸塞州剑桥:麻省理工学院出版社;1989 年。
[Cer89] Ceruzzi Paul. Beyond the Limits—Flight Enters the ComputerAge. Cambridge, MA: MIT Press; 1989.
[CF58]Curry Haskell B.,Feys Robert。《组合逻辑》,《逻辑与数学基础研究》第 1 卷。荷兰:北荷兰,阿姆斯特丹;1958 年,其中两部分由 William Craig 撰写。
[CF58] Curry Haskell B., Feys Robert. Combinatory Logic, volume 1 of Studies in Logic and the Foundations of Mathematics. The Netherlands: North-Holland, Amsterdam; 1958 With two sections by William Craig.
[CFR + 91]Cytron Ronald、Ferrante Jeanne、Rosen Barry K.、Wegman Mark N.、Kenneth Zadeck F. 高效计算静态单分配形式和控制依赖图。ACM编程语言和系统汇刊。1991;13(4):451-490 十月。
[CFR+91] Cytron Ronald, Ferrante Jeanne, Rosen Barry K., Wegman Mark N., Kenneth Zadeck F. Efficiently computing static single assignment form and the control dependence graph. ACM Transactions on Programming Languages and Systems. 1991;13(4):451–490 October.
[氟钨酸铵12]Tom Christiansen、Foy Brian d、Larry Wall 和 Jon Orwant。《Perl 编程》。第四版 Sebastopol,加州:O'Reilly Media;2012 年。
[CfWO12] Christiansen Tom, foy brian d, Wall Larry, Orwant Jon. Programming Perl. fourth edition Sebastopol, CA: O'Reilly Media; 2012.
[Che92]Andrew Cheese。Parlog的并行执行。德国柏林:Springer-Verlag;1992 年。
[Che92] Cheese Andrew. Parallel Execution of Parlog. Berlin, Germany: Springer-Verlag; 1992.
[Cho56]诺姆·乔姆斯基。描述语言的三种模型。IRE信息理论汇刊。1956年;IT-2(3):9 月 113–124 日。
[Cho56] Chomsky Noam. Three models for the description of language. IRE Transactions on Information Theory. 1956;IT-2(3):113–124 September.
[Cho62]乔姆斯基·诺姆。上下文无关语法和下推存储。见:季度进展报告第 65 号。马萨诸塞州剑桥:麻省理工学院电子研究实验室;1962 年:187-194。
[Cho62] Chomsky Noam. Context-free grammars and pushdown storage. In: Quarterly Progress Report No. 65. Cambridge, MA: MIT Research Laboratory for Electronics; 1962:187–194.
[第 71 章]Courtois Pierre-Jacques、Heymans F.、Parnas David L. “读者”和“写者”的并发控制。ACM通讯。1971;14(10):10 月 667-668 日。
[CHP71] Courtois Pierre-Jacques, Heymans F., Parnas David L. Concurrent control with ‘readers’ and ‘writers’. Communications of the ACM. 1971;14(10):667–668 October.
[楚41]Church Alonzo。《Lambda 转换演算》。新泽西州普林斯顿:普林斯顿大学出版社;1941 年《数学研究年鉴》第 6 期。
[Chu41] Church Alonzo. The Calculi of Lambda-Conversion. Princeton, NJ: Princeton University Press; 1941 Annals of Mathematical Studies #6.
[CL83]Cook Robert P.,LeBlanc Thomas J. 符号表抽象用于实现具有明确范围控制的语言。IEEE软件工程学报。1983;SE–9(1):1 月 8 日到 12 日。
[CL83] Cook Robert P., LeBlanc Thomas J. A symbol table abstraction to implement languages with explicit scope control. IEEE Transactions on Software Engineering. 1983;SE–9(1):8–12 January.
[Cle86]Craig Cleaveland J.数据类型简介。马萨诸塞州雷丁:Addison-Wesley;1986 年。
[Cle86] Craig Cleaveland J. An Introduction to Data Types. Reading, MA: Addison-Wesley; 1986.
[CLFL94]Chase Jeffrey S.、Levy Henry M.、Feeley Michael J.、Lazowska Edward D.。单地址空间操作系统中的共享和保护。ACM计算机系统学报。1994;12(4):271–307 十一月。
[CLFL94] Chase Jeffrey S., Levy Henry M., Feeley Michael J., Lazowska Edward D. . Sharing and protection in a single-address-space operating system. ACM Transactions on Computer Systems. 1994;12(4):271–307 November.
[CM84]Mani Chandy K.,Misra Jayadev。《饮酒哲学家问题》。《ACM 编程语言和系统汇刊》。1984;6(4):632-646 年 10 月。
[CM84] Mani Chandy K., Misra Jayadev. The drinking philosophers problem. ACM Transactions on Programming Languages and Systems. 1984;6(4):632–646 October.
[CM03]Clocksin William F.、Mellish Christopher S. 《Prolog 编程》。第五版德国:Springer-Verlag,柏林;2003 年。
[CM03] Clocksin William F., Mellish Christopher S. Programming in Prolog. fifth edition Germany: Springer-Verlag, Berlin; 2003.
[Coh81]Cohen Jacques。链接数据结构的垃圾收集。ACM计算调查。1981;13(3):9 月 341-367 日。
[Coh81] Cohen Jacques. Garbage collection of linked data structures. ACM Computing Surveys. 1981;13(3):341–367 September.
[Con63]Conway Melvin E. 可分离转换图编译器的设计。ACM通讯。1963;6(7):7 月 396-408 日。
[Con63] Conway Melvin E. Design of a separable transition-diagram compiler. Communications of the ACM. 1963;6(7):396–408 July.
[Cou84]Courcelle Bruno。属性语法:定义、依赖关系分析、证明方法。收录于:Lorho Bernard 主编的《编译器构造方法与工具:高级课程》。英国剑桥:剑桥大学出版社;1984 年:81-102。
[Cou84] Courcelle Bruno. Attribute grammars: Definitions, analysis of dependencies, proof methods. In: Lorho Bernard, ed. Methods and Tools for Compiler Construction: An Advanced Course. Cambridge, England: Cambridge University Press; 1984:81–102.
[CS69]Cocke John,Schwartz Jacob T.编程语言及其编译器:初步说明。纽约,纽约州:纽约大学 Courant 数学科学研究所;1969 年技术报告。
[CS69] Cocke John, Schwartz Jacob T. Programming languages and their compilers: Preliminary notes. New York, NY: Courant Institute of Mathematical Sciences, New York University; 1969 Technical report.
[CS01]Chamberlain Bradford L.,Snyder Lawrence。数组语言支持并行稀疏计算。见:第十五届国际超级计算大会论文集;2001:133–145 意大利索伦托,六月。
[CS01] Chamberlain Bradford L., Snyder Lawrence. Array language support for parallel sparse computation. In: Proceedings of the Fifteenth International Conference on Supercomputing; 2001:133–145 Sorrento, Italy, June.
[CT04]Cooper Keith D.、Torczon Linda。《设计编译器》。加利福尼亚州旧金山:Morgan Kaufmann;2004 年。
[CT04] Cooper Keith D., Torczon Linda. Engineering a Compiler. San Francisco, CA: Morgan Kaufmann; 2004.
[CW85]Cardelli Luca,Wegner Peter。关于理解类型、数据抽象和多态性。ACM计算调查。1985;17(4):471-522 十二月。
[CW85] Cardelli Luca, Wegner Peter. On understanding types, data abstraction, and polymorphism. ACM ComputingSurveys. 1985;17(4):471–522 December.
[CW05]Chonacky Norman,Winch David。Maple、Mathematica 和 Matlab:没有磁带的 3M。科学与工程计算。2005;7(1):8-16。
[CW05] Chonacky Norman, Winch David. Maple, Mathematica, and Matlab: The 3M's without the tape. Computing in Science and Engineering. 2005;7(1):8–16.
[Dar90]Darlington Jared L. 面向目标编程中按目标失败搜索方向。ACM编程语言和系统事务。1990;12(2):224-252 四月。
[Dar90] Darlington Jared L. Search direction by goal failure in goal-oriented programming. ACM Transactions on Programming Languages and Systems. 1990;12(2):224–252 April.
[Dav63]戴维斯·马丁。《从机械证明中消除无关因素》。《应用数学研讨会论文集》,第 15 卷;罗德岛州普罗维登斯:美国数学学会;1963 年:15-30 页。
[Dav63] Davis Martin. Eliminating the irrelevant from mechanical proofs. In: Proceedings of a Symposium in Applied Mathematics, volume 15; Providence, RI: American Mathematical Society; 1963:15–30.
[DB76]Peter Deutsch L.,Bobrow Daniel G. 一种高效的增量式自动垃圾收集器。《ACM 通讯》。1976;19(9):522-526 年 9 月。
[DB76] Peter Deutsch L., Bobrow Daniel G. An efficient incremental automatic garbage collector. Communications of the ACM. 1976;19(9):522–526 September.
[DDH72]Dahl Ole-Johan、Dijkstra Edsger W.、Hoare Charles Antony Richard。结构化编程。纽约:Academic Press;1972 APIC数据处理研究#8。
[DDH72] Dahl Ole-Johan, Dijkstra Edsger W., Hoare Charles Antony Richard. Structured Programming. New York, NY: Academic Press; 1972 A.P.I.C. Studies in Data Processing #8.
[DeR71]DeRemer Franklin L. 简单的 LR(k) 语法。《ACM 通讯》。1971;14(7):7 月 453-460。
[DeR71] DeRemer Franklin L. Simple LR(k) grammars. Communications of the ACM. 1971;14(7):453–460 July.
[DGAFS + 80]Dewar Robert BK、Fisher Jr. Gerald A.、Schonberg Edmond、Froehlich Robert、Bryant Stephen、Goss Clinton F.、Burke Michael。纽约大学 Ada 翻译器和解释器。收录于:ACM SIGPLAN Ada 编程语言研讨会论文集;1980:194–201 马萨诸塞州波士顿,12 月。
[DGAFS+80] Dewar Robert B.K., Fisher Jr. Gerald A., Schonberg Edmond, Froehlich Robert, Bryant Stephen, Goss Clinton F., Burke Michael. The NYU Ada translator and interpreter. In: Proceedings of the ACM SIGPLAN Symposium on the Ada Programming Language; 1980:194–201 Boston, MA, December.
[Dij60]Dijkstra Edsger W. 递归编程。Numerische Mathematik。1960;2:312-318 重印于《编程系统和语言》第 221-228 页,Saul Rosen 编辑。麦格劳-希尔,纽约,1967 年。
[Dij60] Dijkstra Edsger W. Recursive programming. Numerische Mathematik. 1960;2:312–318 Reprinted as pages 221–228 of Programming Systems and Languages, Saul Rosen, editor. McGraw-Hill, New York, NY, 1967.
[Dij65]Dijkstra Edsger W. 并发编程控制中问题的解决。ACM通讯。1965;8(9):569 九月。
[Dij65] Dijkstra Edsger W. Solution ofaproblem in concurrent programming control. Communications of the ACM. 1965;8(9):569 September.
[Dij68a]Dijkstra Edsger W. 协同顺序进程。引自:Genuys F. 编辑。编程语言。英国伦敦:Academic Press;1968:43-112。
[Dij68a] Dijkstra Edsger W. Co-operating sequential processes. In: Genuys F., ed. Programming Languages. London, England: Academic Press; 1968:43–112.
[Dij68b]Dijkstra Edsger W.《转到声明被认为有害》。《ACM 通讯》。1968;11(3):3 月 147-148 日。
[Dij68b] Dijkstra Edsger W. Go To statement considered harmful. Communications of the ACM. 1968;11(3):147–148 March.
[Dij72]Dijkstra Edsger W. 顺序进程的层次化排序。收录于:Hoare Charles Antony Richard、Perrott Ronald H. 编。操作系统技术。英国伦敦:Academic Press;1972:72-93 APIC 数据处理研究 #9 另见Acta Informatica,1(8):115-138,1971 年。
[Dij72] Dijkstra Edsger W. Hierarchical ordering of sequential processes. In: Hoare Charles Antony Richard, Perrott Ronald H., eds. Operating Systems Techniques. London, England: Academic Press; 1972:72–93 A.P.I.C. Studies in Data Processing #9 Also Acta Informatica, 1(8):115–138, 1971.
[Dij75]Dijkstra Edsger W. 受保护的命令、不确定性和程序的形式化推导。《ACM 通讯》。1975;18(8):8 月 453-457 日。
[Dij75] Dijkstra Edsger W. Guarded commands, nondeterminacy, and formal derivation of programs. Communications of the ACM. 1975;18(8):453–457 August.
[Dij76]Dijkstra Edsger W. 《编程原则》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1976 年。
[Dij76] Dijkstra Edsger W. A Discipline of Programming. Englewood Cliffs, NJ: Prentice-Hall; 1976.
[Dij82]Dijkstra Edsger W. 我们如何说出可能伤人的真相?。ACM SIGPLAN 通知。1982;17(5):5 月 13-15 日。
[Dij82] Dijkstra Edsger W. How do we tell truths that might hurt?. ACM SIGPLAN Notices. 1982;17(5):13–15 May.
[Dio78]Dion Bernard A.上下文无关和上下文敏感解析器的局部最小成本错误校正器。威斯康星大学麦迪逊分校;1978 年博士论文《计算机科学技术报告》第 344 号。
[Dio78] Dion Bernard A. Locally Least-Cost Error Correctors for Context-Free and Context-Sensitive Parsers. University of Wisconsin–Madison; 1978 Ph. D. dissertation Computer Sciences Technical Report #344.
[DMM96]Diwan Amer、Eliot J.、Moss B.、McKinley Kathryn S。简单有效地分析静态类型的面向对象程序。收录于:第十一届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1996:292–305 加利福尼亚州圣何塞,十月。
[DMM96] Diwan Amer, Eliot J., Moss B., McKinley Kathryn S. Simple and effective analysis of statically typed object-oriented programs. In: Proceedings of the Eleventh ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1996:292–305 San Jose, CA, October.
[Dol97]Dolby Julian。对象的自动内联分配。摘自:SIGPLAN '97 编程语言设计和实现会议论文集;1997 年:7-17 拉斯维加斯,内华达州,六月。
[Dol97] Dolby Julian. Automatic inline allocation of objects. In: Proceedings of the SIGPLAN '97 Conference on Programming Language Design and Implementation; 1997:7–17 Las Vegas, NV, June.
[Dri93]Driesen Karel。选择器表索引和稀疏数组。收录于:第八届 ACM SIGPLAN 面向对象编程系统、语言和应用程序会议论文集;1993:259–270 华盛顿特区,九月。
[Dri93] Driesen Karel. Selector table indexing and sparse arrays. In: Proceedings of the Eighth ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages, and Applications; 1993:259–270 Washington, DC, September.
[DRSS96]Dawson Steven、Ramakrishnan CR、Skiena Steven、Swift Terrence。统一因式分解的原理和实践。ACM编程语言和系统事务。1996;18(5):9 月 528-563 日。
[DRSS96] Dawson Steven, Ramakrishnan C.R., Skiena Steven, Swift Terrence. Principles and practice of unification factoring. ACM Transactions on Programming Languages and Systems. 1996;18(5):528–563 September.
[DS84]Peter Deutsch L.,Schiffman Allan M. Smalltalk-80 系统的有效实现。见:第 11 届 ACM 编程语言原理研讨会会议记录;1984:297-302 犹他州盐湖城,1 月。
[DS84] Peter Deutsch L., Schiffman Allan M. Efficient implementation of the Smalltalk-80 system. In: Conference Record of the Eleventh ACM Symposium on Principles of Programming Languages; 1984:297–302 Salt Lake City, UT, January.
[DSS06]Dice Dave、Shalev Ori、Shavit Nir。事务锁定 II。摘自:第二十届国际分布式计算研讨会论文集;2006 年 9 月,瑞典斯德哥尔摩,第 194-208 页。
[DSS06] Dice Dave, Shalev Ori, Shavit Nir. Transactional locking II. In: Proceedings of the Twentieth International Symposium on Distributed Computing; 2006:194–208 Stockholm, Sweden, September.
[到期05]Duesterwald Evelyn。动态二进制优化器的设计和工程。IEEE论文集。2005;93(2):2 月 436-448。
[Due05] Duesterwald Evelyn. Design and engineering of a dynamic binary optimizer. Proceedings of the IEEE. 2005;93(2):436–448 February.
[DWA10]DWARF 调试信息格式委员会。DWARF调试信息格式,版本 4。2010年 6 月,可从dwarfstd.org/doc/DWARF4.pdf获取。
[DWA10] DWARF Debugging Information Format Committee. DWARF Debugging Information Format, Version 4. 2010. June Available as dwarfstd.org/doc/DWARF4.pdf.
[Dya95]Dyadkin Lev J. 多框解析器:不再需要手写词法分析器。IEEE软件。1995;12(5):9 月 61-67 日。
[Dya95] Dyadkin Lev J. Multibox parsers: No more handwritten lexical analyzers. IEEE Software. 1995;12(5):61–67 September.
[Eag12]Eager Michael J. DWARF 调试格式简介。The Pragmatic Programmers;2012 年 4 月 可从dwarfstd.org/doc/Debugging%20using%20DWARF-2012.pdf获取。
[Eag12] Eager Michael J. Introduction to the DWARF debugging format. The Pragmatic Programmers; 2012. April Available as dwarfstd.org/doc/Debugging%20using%20DWARF-2012.pdf.
[耳朵70]Earley Jay。一种高效的上下文无关解析算法。ACM通讯。1970年 2 月 13(2):94-102。
[Ear70] Earley Jay. An efficient context-free parsing algorithm. Communications of the ACM. 1970;13(2):94–102 February.
[ECM06a]ECMA International,瑞士日内瓦。C # 语言规范。第四版,2006 年 6 月 ECMA-334、ISO/IEC 23270。可用作ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf。
[ECM06a] ECMA International, Geneva, Switzerland. C# Language Specification. fourth edition 2006. June ECMA-334, ISO/IEC 23270. Available as ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf.
[ECM06b]ECMA International,瑞士日内瓦。Eiffel :分析、设计和编程语言。第二版,2006 年 6 月 ECMA-367。可在ecma-international.org/publications/standards/Ecma-367.htm上找到。
[ECM06b] ECMA International, Geneva, Switzerland. Eiffel: Analysis, Design and Programming Language. second edition 2006. June ECMA-367. Available at ecma-international.org/publications/standards/Ecma-367.htm.
[ECM11]ECMA International,瑞士日内瓦。ECMAScript语言规范。2011年 5.1 版。6 月 ECMA-262,ISO/IEC 16262:2011。可从ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf获取。
[ECM11] ECMA International, Geneva, Switzerland. ECMAScript Language Specification. 5.1 edition 2011. June ECMA-262, ISO/IEC 16262:2011. Available as ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf.
[ECM13]ECMA International,瑞士日内瓦。JSON数据交换格式。2013年 10 月 ECMA-404。可从ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf获取。
[ECM13] ECMA International, Geneva, Switzerland. The JSON Data Interchange Format. 2013. October ECMA-404. Available as ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf.
[英语84]Engelfriet Joost。属性语法:属性评估方法。收录于:Lorho Bernard 主编的《编译器构造方法与工具:高级课程》。英国剑桥:剑桥大学出版社;1984 年:103-138。
[Eng84] Engelfriet Joost. Attribute grammars: Attribute evaluation methods. In: Lorho Bernard, ed. Methods and Tools for Compiler Construction: An Advanced Course. Cambridge, England: Cambridge University Press; 1984:103–138.
[Enn06]Ennals Robert。软件事务内存不应无锁。英特尔剑桥研究院;2006 年技术报告 IRC-TR-06-052。
[Enn06] Ennals Robert. Software transactional memory should not be lock free. Intel Research Cambridge; 2006 Technical Report IRC-TR-06-052.
[ES90]Ellis Margaret A.、Stroustrup Bjarne。《带注释的 C++ 参考手册》。马萨诸塞州雷丁:Addison-Wesley;1990 年。
[ES90] Ellis Margaret A., Stroustrup Bjarne. The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley; 1990.
[伊芙63]James Evey R.下推式存储机的应用。收录于:1963 年秋季联合计算机会议论文集;新泽西州蒙特维尔:AFIPS 出版社;1963 年 11 月,内华达州拉斯维加斯:215–227。
[Eve63] James Evey R. Application of pushdown store machines. In: Proceedings of the 1963 Fall Joint Computer Conference; Montvale, NJ: AFIPS Press; 1963:215–227 Las Vegas, NV, November.
[整箱10]Fischer Charles N.、Cytron Ron K.、LeBlanc Richard J. Jr. 《制作编译器》。第二版,马萨诸塞州波士顿:Addison-Wesley;2010 年。
[FCL10] Fischer Charles N., Cytron Ron K., LeBlanc Richard J. Jr. Crafting a Compiler. second edition Boston, MA: Addison-Wesley; 2010.
[FCO90]Feo John T.、Cann David、Oldehoeft Rod R。Sisal 语言项目报告。并行和分布式计算杂志。1990;10(4):12 月 349-365。
[FCO90] Feo John T., Cann David, Oldehoeft Rod R. A report on the Sisal language project. Journal of Parallel and Distributed Computing. 1990;10(4):349–365 December.
[FG84]Feuer Alan R.、Gehani Narain 编。比较和评估编程语言:Ada、C、Pascal。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1984 年 Prentice-Hall 软件系列。
[FG84] Feuer Alan R., Gehani Narain, eds. Comparing and Assessing Programming Languages: Ada, C, Pascal. Englewood Cliffs, NJ: Prentice-Hall; 1984 Prentice-Hall Software Series.
[FH95]Fraser Christopher W.,Hanson David R.可重定向 C 编译器:设计和实现。加利福尼亚州雷德伍德城:Benjamin/Cummings;1995 年。
[FH95] Fraser Christopher W., Hanson David R. A Retargetable C Compiler: Design and Implementation. Redwood City, CA: Benjamin/Cummings; 1995.
[FHP92]Fraser Christopher W.、Hanson David R.、Proebsting Todd A. 设计一个简单、高效的代码生成器。ACM编程语言和系统快报。1992;1(3):9 月 213-226 日。
[FHP92] Fraser Christopher W., Hanson David R., Proebsting Todd A. Engineering a simple, efficient code generator generator. ACM Letters on Programming Languages and Systems. 1992;1(3):213–226 September.
[Fie00]Fielding Roy Thomas。《架构风格和基于网络的软件架构设计》。尔湾:加利福尼亚大学;2000 年博士论文。
[Fie00] Fielding Roy Thomas. Architectural Styles and the Design of Network-based Software Architectures. Irvine: University of California; 2000 Ph. D. dissertation.
[金融96]Finkel Raphael A.高级编程语言设计。加州门洛帕克:Addison-Wesley;1996 年。
[Fin96] Finkel Raphael A. Advanced Programming Language Design. Menlo Park, CA: Addison-Wesley; 1996.
[FL80]Fischer Charles N.,Jr Richard J. LeBlanc。Pascal 中的运行时诊断实现。IEEE软件工程学报。1980;SE–6(4):7 月 313–319。
[FL80] Fischer Charles N., Jr Richard J. LeBlanc. Implementation of runtime diagnostics in Pascal. IEEE Transactions on Software Engineering. 1980;SE–6(4):313–319 July.
[Fle76]Fleck Arthur C. 论通过按名称参数传输技术进行内容交换的不可能性。ACM SIGPLAN 通知。1976;11(11):11 月 38-41 日。
[Fle76] Fleck Arthur C. On the impossibility of content exchange through the by-name parameter transmission technique. ACM SIGPLAN Notices. 1976;11(11):38–41 November.
[FMQ80]Fischer Charles N.、Milton Donn R.、Quiring Sam B. 仅使用插入即可实现高效的 LL(1) 错误更正和恢复。Acta Informatica。1980;13(2):2 月 141-154。
[FMQ80] Fischer Charles N., Milton Donn R., Quiring Sam B. Efficient LL(1) error correction and recovery using only insertions. Acta Informatica. 1980;13(2):141–154 February.
[Fra80]Francez Nissim。分布式终止。ACM编程语言和系统事务。1980;2(1):1 月 42-55 日。
[Fra80] Francez Nissim. Distributed termination. ACM Transactions on Programming Languages and Systems. 1980;2(1):42–55 January.
[FRF08]Felber Pascal、Riegel Torvald、Fetzer Christof。基于字的软件事务内存的动态性能调优。收录于:第十三届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2008 年:237–246 犹他州盐湖城,二月。
[FRF08] Felber Pascal, Riegel Torvald, Fetzer Christof. Dynamic performance tuning of word-based software transactional memory. In: Proceedings of the Thirteenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2008:237–246 Salt Lake City, UT, February.
[FSS83]Freudenberger Stefan M.、Schwartz Jacob T.、Sharir Micha。《SETL 优化器使用经验》。《ACM 编程语言和系统汇刊》。1983;5(1):1 月 26-45 日。
[FSS83] Freudenberger Stefan M., Schwartz Jacob T., Sharir Micha. Experience with the SETL optimizer. ACM Transactions on Programming Languages and Systems. 1983;5(1):26–45 January.
[FWH01]Friedman Daniel P.、Wand Mitchell、Haynes Christopher T.《编程语言基本原理》。第二版,马萨诸塞州剑桥:麻省理工学院出版社;2001 年。
[FWH01] Friedman Daniel P., Wand Mitchell, Haynes Christopher T. Essentials of Programming Languages. second edition Cambridge, MA: MIT Press; 2001.
[69 财年]Fenichel Robert R.,Yochelson Jerome C. 用于虚拟内存计算机系统的 Lisp 垃圾收集器。ACM通讯。1969;12(11):611-612 十一月。
[FY69] Fenichel Robert R., Yochelson Jerome C. A Lisp garbage collector for virtual memory computer systems. Communications of the ACM. 1969;12(11):611–612 November.
[英镑+ 94]Geist Al、Beguelin Adam、Dongarra Jack、Jiang Weicheng、Manchek Robert、Sunderam Vaidyalingam S. PVM:并行虚拟机:网络并行计算用户指南和教程。马萨诸塞州剑桥:麻省理工学院出版社;1994 年。可在netlib.org/pvm3/book/pvm-book.html上获取。
[GBD+94] Geist Al, Beguelin Adam, Dongarra Jack, Jiang Weicheng, Manchek Robert, Sunderam Vaidyalingam S. PVM: Parallel Virtual Machine: A Users' Guide and Tutorial for Networked Parallel Computing. Cambridge, MA: MIT Press; 1994. Available at netlib.org/pvm3/book/pvm-book.html.
[GBJ + 12]Grune Dick、Bal Henri E.、Jacobs Ceriel JH、Langendoen Koen G.、Reeuwijk Kees van。现代编译器设计。第二版纽约州纽约:施普林格出版社; 2012年。
[GBJ+12] Grune Dick, Bal Henri E., Jacobs Ceriel J.H., Langendoen Koen G., Reeuwijk Kees van. Modern Compiler Design. second edition New York, NY: Springer-Verlag; 2012.
[GDDC97]Grove David、DeFouw Greg、Dean Jeffrey、Chambers Craig。面向对象语言中的调用图构造。收录于:第十二届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1997:108–124 亚特兰大,佐治亚州,十月。
[GDDC97] Grove David, DeFouw Greg, Dean Jeffrey, Chambers Craig. Call graph construction in object-oriented languages. In: In Proceedings of the Twelfth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1997:108–124 Atlanta, GA, October.
[GFH82]Ganapathi Mahadevan、Fischer Charles N.、Hennessy John L. 可重定向编译器代码生成。ACM计算调查。1982;14(4):573-592 十二月。
[GFH82] Ganapathi Mahadevan, Fischer Charles N., Hennessy John L. Retargetable compiler code generation. ACM Computing Surveys. 1982;14(4):573–592 December.
[GG78]Steven Glanville R.,Graham Susan L.一种编译器代码生成的新方法。摘自:第五届 ACM 编程语言原理研讨会会议记录;1978:231–240 亚利桑那州图森,1 月。
[GG78] Steven Glanville R., Graham Susan L. A new method for compiler code generation. In: Conference Record of the Fifth Annual ACM Symposium on Principles of Programming Languages; 1978:231–240 Tucson, AZ, January.
[GG96]Griswold Ralph E.、Griswold Madge T. Icon 编程语言。第三版,加利福尼亚州圣何塞:Peer-to-Peer Communications;1996 年已绝版;可在线访问 cs.arizona.edu/icon/lb3.htm。先前版本由 Prentice-Hall 出版。
[GG96] Griswold Ralph E., Griswold Madge T. The Icon Programming Language. third edition San Jose, CA: Peer-to-Peer Communications; 1996 Out of print; available on-line at cs.arizona.edu/icon/lb3.htm. Previous editions published by Prentice-Hall.
[国金利+03 ]Garcia Ronald、Jarvi Jaakko、Lumsdaine Andrew、Siek Jeremy、Willcock Jeremiah。通用编程语言支持的比较研究。收录于:第十八届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;2003 年 10 月,加利福尼亚州阿纳海姆,115–134 页。
[GJL+03] Garcia Ronald, Jarvi Jaakko, Lumsdaine Andrew, Siek Jeremy, Willcock Jeremiah. A comparative study of language support for generic programming. In: Proceedings of the Eighteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 2003:115–134 Anaheim, CA, October.
[加沙地带+ 14]Gosling James、Joy Bill、Steele Guy、Bracha Gilad、Buckley Alex。Java语言规范。Java SE 8 版,马萨诸塞州雷丁:Addison-Wesley;2014 年。可在docs.oracle.com/javase/specs/上获取。
[GJS+14] Gosling James, Joy Bill, Steele Guy, Bracha Gilad, Buckley Alex. The Java Language Specification. Java SE 8 edition Reading, MA: Addison-Wesley; 2014. Available at docs.oracle.com/javase/specs/.
[GL05]Guyer Samuel Z.,Lin Calvin。客户端驱动指针分析。计算机编程科学。2005;58(1–2):10 月 83–114 日。
[GL05] Guyer Samuel Z., Lin Calvin. Client-driven pointer analysis. Science of Computer Programming. 2005;58(1–2):83–114 October.
[GLDW87]Gingell Robert A.、Lee Meng、Dang Xuong T.、Weeks Mary S. SunOS 中的共享库。摘自:1987 年夏季 USENIX 会议论文集;1987:131–145 亚利桑那州凤凰城,六月。
[GLDW87] Gingell Robert A., Lee Meng, Dang Xuong T., Weeks Mary S. Shared libraries in SunOS. In: Proceedings of the 1987 Summer USENIX Conference; 1987:131–145 Phoenix, AZ, June.
[GM86]Gibbons Phillip B.、Muchnick Steven S.流水线架构的高效指令调度。摘自:SIGPLAN '86 编译器构建研讨会论文集;1986 年 7 月 11-16 日,加利福尼亚州帕洛阿尔托。
[GM86] Gibbons Phillip B., Muchnick Steven S. Efficient instruction scheduling for a pipelined architecture. In: Proceedings of the SIGPLAN '86 Symposium on Compiler Construction; 1986:11–16 Palo Alto, CA, July.
[Gol84]Goldberg Adele。Smalltalk -80:交互式编程环境。马萨诸塞州雷丁:Addison-Wesley;1984 年 Addison-Wesley 计算机科学系列。
[Gol84] Goldberg Adele. Smalltalk-80: The Interactive Programming Environment. Reading, MA: Addison-Wesley; 1984 Addison-Wesley Series in Computer Science.
[Gol91]Goldberg David。《每个计算机科学家都应该知道的浮点运算》。ACM计算调查。1991;23(1):3 月 5-48 日。
[Gol91] Goldberg David. What every computer scientist should know about floating-point arithmetic. ACM Computing Surveys. 1991;23(1):5–48 March.
[Gol12] David Goldberg。计算机算术。在 Hennessy 和 Patterson [HP12] 中,附录 J。可从booksite.mkp.com/9780123838728/references/appendix_j.pdf获取。
[Gol12] David Goldberg. Computer arithmetic. In Hennessy and Patterson [HP12], Appendix J. Available as booksite.mkp.com/9780123838728/references/appendix_j.pdf.
[Goo75]Goodenough John B. 异常处理:问题和建议的表示法。《ACM 通讯》。1975年 12 月 18(12):683-696。
[Goo75] Goodenough John B. Exception handling: Issues and a proposed notation. Communications of the ACM. 1975;18(12):683–696 December.
[Gor79]Gordon Michael JC 《编程语言的外延描述:简介》。纽约:Springer-Verlag;1979 年。
[Gor79] Gordon Michael J.C. The Denotational Description of Programming Languages: An Introduction. New York, NY: Springer-Verlag; 1979.
[GPP71]Griswold Ralph E.、Poage JF、Polonsky IP 《Snobol4 编程语言》。第二版 Englewood Cliffs,新泽西州:Prentice-Hall;1971 年。
[GPP71] Griswold Ralph E., Poage J.F., Polonsky I.P. The Snobol4 Programming Language. second edition Englewood Cliffs, NJ: Prentice-Hall; 1971.
[GR62]Ginsburg Seymour,Gordon Rice H. 与 ALGOL 相关的两种语言。《ACM 杂志》。1962;9(3):350–371。
[GR62] Ginsburg Seymour, Gordon Rice H. Two families of languages related to ALGOL. Journal of the ACM. 1962;9(3):350–371.
[89]Goldberg Adele,Robson David。Smalltalk -80:语言。马萨诸塞州雷丁:Addison-Wesley 计算机科学丛书。Addison-Wesley;1989 年。
[GR89] Goldberg Adele, Robson David. Smalltalk-80: The Language. Reading, MA: Addison-Wesley Series in Computer Science. Addison-Wesley; 1989.
[Gri81]Gries David。《编程科学》。纽约:计算机科学文本和专著。Springer-Verlag;1981 年。
[Gri81] Gries David. The Science of Programming. New York, NY: Textsand Monographs in Computer Science. Springer-Verlag; 1981.
[GS99]Gil Joseph (Yossi),Sweeney Peter F.多重继承的空间和时间高效内存布局。收录于:第十四届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1999:256–275 丹佛,科罗拉多州,十一月。
[GS99] Gil Joseph (Yossi), Sweeney Peter F. Space- and time-efficient memory layout for multiple inheritance. In: Proceedings of the Fourteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1999:256–275 Denver, CO, November.
[GSB + 93]Garrett William E.、Scott Michael L.、Bianchini Ricardo、Kontothanassis Leonidas I.、Andrew McCallum R.、Thomas Jeffrey A.、Wisniewski Robert、Luk Steve。链接共享片段。引自:USENIX 1993 年冬季技术会议论文集;1993 年 1 月 13-27 日,加利福尼亚州圣地亚哥。
[GSB+93] Garrett William E., Scott Michael L., Bianchini Ricardo, Kontothanassis Leonidas I., Andrew McCallum R., Thomas Jeffrey A., Wisniewski Robert, Luk Steve. Linking shared segments. In: Proceedings of the USENIX Winter '93 Technical Conference; 1993:13–27 San Diego, CA, January.
[GTW78]Goguen Joseph A.、Thatcher James W.、Wagner Eric G. 初步代数方法,用于规范、正确性和抽象数据类型的实现。收录于:Yeh Raymond T. 主编。Englewood Cliffs,新泽西州:Prentice-Hall;80–149。编程方法论的最新趋势。1978年;第 4 卷。
[GTW78] Goguen Joseph A., Thatcher James W., Wagner Eric G. An initial algebra approach to the specification, correctness, and implementation ofabstract data types. In: Yeh Raymond T., ed. Englewood Cliffs, NJ: Prentice-Hall; 80–149. Current Trends in Programming Methodology. 1978;volume 4.
[Gut77]Guttag John。抽象数据类型和数据结构的发展。ACM通讯。1977;20(6):396-404 六月。
[Gut77] Guttag John. Abstract data types and the development of data structures. Communications of the ACM. 1977;20(6):396–404 June.
[Hal85]Jr Robert H. Halstead。Multilisp:一种用于并发符号计算的语言。ACM编程语言和系统事务。1985;7(4):10 月 501-538 日。
[Hal85] Jr Robert H. Halstead. Multilisp: A language for concurrent symbolic computation. ACM Transactions on Programming Languages and Systems. 1985;7(4):501–538 October.
[韩81]Hanson David R. 块结构是必要的吗?。软件——实践与经验。1981;11(8):853–866 年 8 月。
[Han81] Hanson David R. Is block structure necessary?. Software—Practice and Experience. 1981;11(8):853–866 August.
[Han93] David R. Hanson。Icon 简介。载于 HOPL IIProceedings [Ass93],第 359-360 页。
[Han93] David R. Hanson. A brief introduction to Icon. In HOPL IIProceedings [Ass93], pages 359-360.
[哈92]Harbison Samuel P. Modula-3。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1992 年。
[Har92] Harbison Samuel P. Modula-3. Englewood Cliffs, NJ: Prentice-Hall; 1992.
[哈01]Harris Timothy L.非阻塞链接列表的实用实现。摘自:第十五届国际分布式计算研讨会 (DISC) 论文集;2001 年 10 月,葡萄牙里斯本,第 300–314 页。
[Har01] Harris Timothy L. A pragmatic implementation of non-blocking linked-lists. In: Proceedings of the Fifteenth International Symposium on Distributed Computing (DISC); 2001:300–314 Lisbon, Portugal, October.
[危险11]Hazelwood Kim。动态二进制修改:工具、技术和应用。Morgan & Claypool 出版社;2011 年 3 月计算机架构综合讲座。
[Haz11] Hazelwood Kim. Dynamic Binary Modification: Tools, Techniques, and Applications. Morgan & Claypool Publishers; 2011 Synthesis Lectures on Computer Architecture March.
[HD68]Hauck EA、Dent BA Burroughs 的 B6500/B7500 堆叠机制。引自:AFIPS 春季联合计算机会议论文集;1968:第 32 卷第 245-251 页,重印为 Siewiorek、Bell 和 Newell [SBN82] 的第 244-250 页。
[HD68] Hauck E.A., Dent B.A. Burroughs' B6500/B7500 stack mechanism. In: Proceedings of the AFIPS Spring Joint Computer Conference; 1968:245–251 volume 32, Reprinted as pages 244–250 of Siewiorek, Bell, and Newell [SBN82].
[HD89]Henry Robert R.,Damron Peter C.使用树形模式匹配的表驱动代码生成器算法。西雅图,华盛顿:华盛顿大学;1989 年技术报告 89-02-03,计算机科学系 2 月。
[HD89] Henry Robert R., Damron Peter C. Algorithms for table-driven code generators using tree-pattern matching. Seattle, WA: University of Washington; 1989 Technical Report 89-02-03, Computer Science Department February.
[亨14]Hendren Laurie 编辑。ACM SIGPLAN 数组编程库、语言和编译器国际研讨会。苏格兰:爱丁堡;2014 年 6 月与第三十五届 ACM SIGPLAN 编程语言设计和实现会议同时举行。
[Hen14] Hendren Laurie, ed. ACM SIGPLAN International Workshop on Libraries, Languages, and Compilers for Array Programming. Scotland: Edinburgh; 2014 June Held in conjunction with the Thirty-Fifth ACM SIGPLAN Conference on Programming Language Design and Implementation.
[Her91]Herlihy Maurice P. 无等待同步。ACM编程语言和系统事务。1991;13(1):124–149 一月。
[Her91] Herlihy Maurice P. Wait-free synchronization. ACM Transactions on Programming Languages and Systems. 1991;13(1):124–149 January.
[HGLS78]Holt Richard C.、Scott Graham G.、Lazowska Edward D.、Scott Mark A.《结构化并发编程与操作系统应用程序》。马萨诸塞州雷丁:Addison-Wesley;1978 Addison-Wesley 计算机科学系列。
[HGLS78] Holt Richard C., Scott Graham G., Lazowska Edward D., Scott Mark A. Structured Concurrent Programming with Operating Systems Applications. Reading, MA: Addison-Wesley; 1978 Addison-Wesley Series in Computer Science.
[HH97a]Hauben Michael、Hauben Ronda。《网民:Usenet 和互联网的历史与影响》。纽约州纽约市:Wiley/IEEE 计算机学会出版社;1997 年。可访问columbia.edu/~hauben/netbook/。
[HH97a] Hauben Michael, Hauben Ronda. Netizens: On the History and Impact of Usenet and the Internet. New York, NY: Wiley/IEEE Computer Society Press; 1997. Available at columbia.edu/~hauben/netbook/.
[HH97b]Hookway Raymond J.,Herdeg Mark A. DIGITAL FX!32:结合仿真和二进制翻译。DIGITAL技术杂志。1997;9(1):3–12。
[HH97b] Hookway Raymond J., Herdeg Mark A. DIGITAL FX!32: Combining emulation and binary translation. DIGITAL Technical Journal. 1997;9(1):3–12.
[Hin01]Hind Michael。指针分析:我们还没有解决这个问题吗?。在:ACM SIGPLAN-SIGSOFT 软件工具和工程程序分析研讨会论文集;2001:54-61 Snowbird,犹他州,六月。
[Hin01] Hind Michael. Pointer analysis: Haven't we solved this problem yet?. In: Proceedings of the ACM SIGPLAN--SIGSOFT Workshop on Program Analysis for Software Tools and Engineering; 2001:54–61 Snowbird, UT, June.
[HJBG81]Hennessy John L.、Jouppi Norman、Baskett Forest、Gill John。MIPS :一种 VLSI 处理器架构。收录于:CMU VLSI 系统与计算会议论文集;马里兰州罗克维尔:计算机科学出版社;1981 年 10 月第 337-346 页。
[HJBG81] Hennessy John L., Jouppi Norman, Baskett Forest, Gill John. MIPS: A VLSI processor architecture. In: Proceedings of the CMU Conference on VLSI Systems and Computations; Rockville, MD: Computer Science Press; 1981:337–346 October.
[HL94]Hill Patricia M.、Lloyd John W. 《哥德尔编程语言》。马萨诸塞州剑桥:麻省理工学院出版社;1994 逻辑编程系列。
[HL94] Hill Patricia M., Lloyd John W. The Godel Programming Language. Cambridge, MA: MIT Press; 1994 Logic Programming Series.
[HLM03]Herlihy Maurice、Luchangco Victor、Moir Mark。无阻碍同步:以双端队列为例。收录于:第二十三届国际分布式计算系统会议论文集;2003 年 5 月,罗德岛州普罗维登斯,522–529。
[HLM03] Herlihy Maurice, Luchangco Victor, Moir Mark. Obstruction-free synchronization: Double-ended queues as an example. In: Proceedings of the Twenty-Third International Conference on Distributed Computing Systems; 2003:522–529 Providence, RI, May.
[HLR10]Harris Tim、Larus James R.、Rajwar Ravi。《事务性内存》。第二版 Morgan & Claypool 出版社;2010 年 12 月计算机架构综合讲座。
[HLR10] Harris Tim, Larus James R., Rajwar Ravi. Transactional Memory. second edition Morgan & Claypool Publishers; December 2010 Synthesis Lectures on Computer Architecture.
[HM93]Herlihy Maurice P.、Eliot J.、Moss B.事务内存:无锁数据结构的架构支持。收录于:第二十届国际计算机架构研讨会论文集;1993:289–300 加利福尼亚州圣地亚哥,五月。
[HM93] Herlihy Maurice P., Eliot J., Moss B. Transactional memory: Architectural support for lock-free data structures. In: Proceedings of the Twentieth International Symposium on Computer Architecture; 1993:289–300 San Diego, CA, May.
[HMPH05]Harris Tim、Marlow Simon、Jones Simon Peyton、Herlihy Maurice。可组合内存事务。收录于:第十届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2005 年:48–60 芝加哥,伊利诺斯州,六月。
[HMPH05] Harris Tim, Marlow Simon, Jones Simon Peyton, Herlihy Maurice. Composable memory transactions. In: Proceedings of the Tenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2005:48–60 Chicago, IL, June.
[HMRC88]Holt Richard C.、Matthews Philip A.、Alan Rosselet J. 和 Cordy James R. 《图灵编程语言:设计和定义》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1988 年。
[HMRC88] Holt Richard C., Matthews Philip A., Alan Rosselet J., Cordy James R. The Turing Programming Language: Design and Definition. Englewood Cliffs, NJ: Prentice-Hall; 1988.
[HMU07]Hopcroft John E.、Motwani Rajeev、Ullman Jeffrey D. 《自动机理论、语言和计算简介》。第三版 波士顿,马萨诸塞州:Pearson/Addison-Wesley;2007 年。
[HMU07] Hopcroft John E., Motwani Rajeev, Ullman Jeffrey D. Introduction to Automata Theory, Languages, and Computation. third edition Boston, MA: Pearson/Addison-Wesley; 2007.
[HO91]Wilson Ho W.,Olsson Ronald A. 真正的动态链接方法。软件实践与经验。1991;21(4):375-390 四月。
[HO91] Wilson Ho W., Olsson Ronald A. An approach to genuine dynamic linking. Software—Practice and Experience. 1991;21(4):375–390 April.
[Hoa69]Hoare Charles Antony Richard。计算机编程的公理基础。ACM通讯。1969;12(10):576–580+ 十月。
[Hoa69] Hoare Charles Antony Richard. An axiomatic basis of computer programming. Communications of the ACM. 1969;12(10):576–580+ October.
[Hoa74]Hoare Charles Antony Richard。监视器:一种操作系统结构概念。ACM通讯。1974;17(10):10 月 549-557 日。
[Hoa74] Hoare Charles Antony Richard. Monitors: An operating system structuring concept. Communications of the ACM. 1974;17(10):549–557 October.
[Hoa75]Hoare Charles Antony Richard。递归数据结构。国际计算机和信息科学杂志。1975;4(2):6 月 105-132。
[Hoa75] Hoare Charles Antony Richard. Recursive data structures. International Journal of Computer and Information Sciences. 1975;4(2):105–132 June.
[Hoa78]Hoare Charles Antony Richard。《通信顺序过程》。《ACM 通讯》。1978;21(8):8 月 666-677 日。
[Hoa78] Hoare Charles Antony Richard. Communicating Sequential Processes. Communications of the ACM. 1978;21(8):666–677 August.
[Hoa81]Hoare Charles Antony Richard。《皇帝的旧衣服》。《ACM 通讯》。1981;24(2):75-83 二月 1980 年图灵奖演讲。
[Hoa81] Hoare Charles Antony Richard. The emperor's old clothes. Communications of the ACM. 1981;24(2):75–83 February The 1980 Turing Award lecture.
[Hoa89]Hoare Charles Antony Richard。编程语言设计技巧。收录于:Jones Cliff B. 编。《计算机科学论文集》。纽约:Prentice-Hall;1989:193-216 基于 1973 年 10 月在马萨诸塞州波士顿举办的第一届 ACM 编程语言原理研讨会上发表的主题演讲。
[Hoa89] Hoare Charles Antony Richard. Hints on programming language design. In: Jones Cliff B., ed. Essays in Computing Science. New York, NY: Prentice-Hall; 1989:193–216 Based on a keynote address presented at the First ACM Symposium on Principles of Programming Languages, Boston, MA, October 1973.
[霍51]Alfred Horn。《关于代数直接并集的真句子》。《符号逻辑杂志》。1951;16(1):3 月 14-21 日。
[Hor51] Horn Alfred. On sentences which are true of direct unions of algebras. Journal of Symbolic Logic. 1951;16(1):14–21 March.
[Hor87]Horowitz Ellis。《编程语言:一次伟大的旅程》。第三版,马里兰州罗克维尔:计算机科学出版社;1987 年计算机软件工程系列。
[Hor87] Horowitz Ellis. Programming Languages: A Grand Tour. third edition Rockville, MD: Computer Science Press; 1987 Computer Software Engineering Series.
[HP12]Hennessy John L.、Patterson David A.计算机体系结构:一种定量方法。第五版旧金山,加利福尼亚州:Morgan Kaufmann;2012 第三版,2003 年。
[HP12] Hennessy John L., Patterson David A. Computer Architecture: A Quantitative Approach. fifth edition San Francisco, CA: Morgan Kaufmann; 2012 Third edition, 2003.
[12 版]Herlihy Maurice、Shavit Nir。《多处理器编程艺术》。修订版,加利福尼亚州旧金山:Morgan Kaufmann;2012 年。
[HS12] Herlihy Maurice, Shavit Nir. The Art of Multiprocessor Programming. revised edition San Francisco, CA: Morgan Kaufmann; 2012.
[HTWG11]Hejlsberg Anders、Torgersen Mads、Wiltamuth Scott 和 Golde Peter。《C# 编程语言》。第四版,马萨诸塞州波士顿:Addison-Wesley;2011 年。
[HTWG11] Hejlsberg Anders, Torgersen Mads, Wiltamuth Scott, Golde Peter. The C# Programming Language. fourth edition Boston, MA: Addison-Wesley; 2011.
[Hud89]Hudak Paul。函数式编程语言的概念、演变和应用。ACM计算调查。1989;21(3):359-411 年 9 月。
[Hud89] Hudak Paul. Conception, evolution, and application of functional programming languages. ACM Computing Surveys. 1989;21(3):359–411 September.
[IBFW91]Ichbiah Jean、Barnes John GP、Firth Robert J. 和 Woodger Mike。《Ada 编程语言设计原理》。英国剑桥:剑桥大学出版社;1991 年。
[IBFW91] Ichbiah Jean, Barnes John G.P., Firth Robert J., Woodger Mike. Rationale for the Design of the Ada Programming Language. Cambridge, England: Cambridge University Press; 1991.
[IBM87]IBM 公司。APL2编程:语言参考。1987 SH20-9227。
[IBM87] IBM Corporation. APL2 Programming: Language Reference. 1987 SH20-9227.
[IEE87]IEEE 标准委员会。IEEE 二进制浮点运算标准。ACM SIGPLAN 通知。1987;22(2):2 月 9-25 日。
[IEE87] IEEE Standards Committee. IEEE standard for binary floating-point arithmetic. ACM SIGPLAN Notices. 1987;22(2):9–25 February.
[英语61]Ingerman Peter Z. Thunks: 一种编译过程语句的方法,并对过程声明进行了一些注释。《ACM 通讯》。1961;4(1):1 月 55-58 日。
[Ing61] Ingerman Peter Z. Thunks: A way of compiling procedure statements with some comments on procedure declarations. Communications of the ACM. 1961;4(1):55–58 January.
[Ins91]电气和电子工程师协会,纽约,纽约州。IEEE /ANSI Scheme 编程语言标准。1991年。IEEE 1178-1990。可访问http://standards.ieee.org/findstds/standard/1178-1990.html。
[Ins91] Institute of Electrical and Electronics Engineers, New York, NY. IEEE/ANSI Standard for the Scheme Programming Language. 1991. IEEE 1178-1990. Available at http://standards.ieee.org/findstds/standard/1178-1990.html.
[智力90]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - Pascal。1990年。ISO/IEC 7185:1990(ANSI/IEEE 770X 的修订和重新设计)。可访问pascal-central.com/docs/iso7185.pdf。
[Int90] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Pascal. 1990. ISO/IEC 7185:1990 (revision and redesignation of ANSI/IEEE 770X). Available as pascal-central.com/docs/iso7185.pdf.
[Int95]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - Prolog - 第 1 部分:通用核心。1995 ISO/IEC 13211-1:1995。
[Int95] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Prolog—Part 1: General Core. 1995 ISO/IEC 13211-1:1995.
[Int96]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - 第 1 部分:Modula-2,基本语言。1996 ISO/IEC 10514-1:1996。
[Int96] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Part 1: Modula-2, Base Language. 1996 ISO/IEC 10514-1:1996.
[Int97]国际标准化组织,瑞士日内瓦。编程语言 Forth。1997 ISO/IEC 15145:1997(ANSI X3.215-1994 的修订和重新设计)。
[Int97] International Organization for Standardization, Geneva, Switzerland. Programming Language Forth. 1997 ISO/IEC 15145:1997 (revision and redesignation of ANSI X3.215–1994).
[Int99]国际标准化组织,瑞士日内瓦。编程语言 - C。1999年 12 月 ISO/IEC 9899:1999(E)。
[Int99] International Organization for Standardization, Geneva, Switzerland. Programming Language—C. 1999 December ISO/IEC 9899:1999(E).
[Int03a]国际标准化组织,瑞士日内瓦。C基本原理。2003年 4 月 ISO/IEC JTC 1/SC 22/WG 14,修订版 5.10。
[Int03a] International Organization for Standardization, Geneva, Switzerland. The C Rationale. 2003 April ISO/IEC JTC 1/SC 22/WG 14, revision 5.10.
[Int03b]国际标准化组织,瑞士日内瓦。信息技术 - 可移植操作系统接口 (POSIX)。2003年第四版。8 月 ISO/IEC 9945-1:2003。另请参阅 IEEE 标准 1003.1(2004 年版)和 The Open Group 技术标准基础规范第 6 期。可从pubs.opengroup.org/onlinepubs/009695399/获取。
[Int03b] International Organization for Standardization, Geneva, Switzerland. Information Technology—Portable Operating System Interface (POSIX). fourth edition 2003. August ISO/IEC 9945-1:2003. Also IEEE standard 1003.1, 2004 Edition, and The Open Group Technical Standard Base Specifications, Issue 6. Available at pubs.opengroup.org/onlinepubs/009695399/.
[Int10]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - Fortran - 第 1 部分:基础语言。2010 ISO/IEC 1539-1:2010。
[Int10] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Fortran—Part 1: Base Language. 2010 ISO/IEC 1539-1:2010.
[Int11]国际标准化组织,瑞士日内瓦。编程语言 - C。2011年 12 月 ISO/IEC 9899:2011。
[Int11] International Organization for Standardization, Geneva, Switzerland. Programming Languages—C. 2011 December ISO/IEC 9899:2011.
[Int12a]国际标准化组织,瑞士日内瓦。信息技术 - 通用语言基础设施 (CLI)。2012年 2 月 ISO/IEC 23271:2012。另请参阅 ECMA-335,第 6 版,2012 年 6 月。可在ecma-international.org/publications/standards/Ecma-335.htm上找到。
[Int12a] International Organization for Standardization, Geneva, Switzerland. Information Technology—Common Language Infrastructure (CLI). 2012. February ISO/IEC 23271:2012. Also ECMA-335, 6th Edition, June 2012. Available at ecma-international.org/publications/standards/Ecma-335.htm.
[Int12b]国际标准化组织,瑞士日内瓦。信息技术——编程语言——Ada。2012年 12 月 ISO/IEC 8652:2012。可在ada-auth.org/standards/ada12.html上获取。
[Int12b] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Ada. 2012. December ISO/IEC 8652:2012. Available at ada-auth.org/standards/ada12.html.
[Int14a]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言、其环境和系统软件接口 - 编程语言 COBOL。2014 ISO/IEC 1989:2014。
[Int14a] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages, Their Environments and System Software Interfaces—Programming Language COBOL. 2014 ISO/IEC 1989:2014.
[Int14b]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - C++。 2014 ISO/IEC 14882:2014。
[Int14b] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—C++. 2014 ISO/IEC 14882:2014.
[Int15]国际标准化组织,瑞士日内瓦。事务内存的 C++ 扩展技术规范。2015年 5 月 ISO/IEC JTC 1/SC 22/WG 21 N4514。
[Int15] International Organization for Standardization, Geneva, Switzerland. Technical Specification for C++ Extensions for Transactional Memory. 2015 May ISO/IEC JTC 1/SC 22/WG 21 N4514.
[Ive62]Iverson Kenneth E. 《一种编程语言》。纽约:John Wiley and Sons;1962 年。
[Ive62] Iverson Kenneth E. A Programming Language. New York, NY: John Wiley and Sons; 1962.
[JBW + 87]Jefferson David、Beckman Brian、Wieland Fred、Blume Leo、DiLoreto Mike、Hontalas Phil、Laroche Pierre、Sturdevant Kathy、Tupman Jack、Warren Van、Wedel John、Younger Herb、Bellenot Steve。分布式模拟和时间扭曲操作系统。摘自:第十一届 ACM 操作系统原理研讨会论文集。1987:77-93 德克萨斯州奥斯汀,11 月。
[JBW+87] Jefferson David, Beckman Brian, Wieland Fred, Blume Leo, DiLoreto Mike, Hontalas Phil, Laroche Pierre, Sturdevant Kathy, Tupman Jack, Warren Van, Wedel John, Younger Herb, Bellenot Steve. Distributed simulation and the Time Warp operating system. In: Proceedings of the Eleventh ACM Symposium on Operating Systems Principles. 1987:77–93 Austin, TX, November.
[JF10]Järvi Jaakko,Freeman John。C++ lambda 表达式和闭包。计算机编程科学。2010;75(9):9 月 762-772。
[JF10] Järvi Jaakko, Freeman John. C++ lambda expressions and closures. Science of Computer Programming. 2010;75(9):762–772 September.
[JG89]Jones Geraint、Goldsmith Michael。Occam2编程。第二版 Englewood Cliffs,新泽西州:Prentice-Hall;1989 年 Prentice-Hall 国际计算机科学丛书。
[JG89] Jones Geraint, Goldsmith Michael. Programming in occam2. second edition Englewood Cliffs, NJ: Prentice-Hall; 1989 Prentice-Hall International Series in Computer Science.
[JGF96]Jones Simon Peyton、Gordon Andrew、Finne Sigbjorn。并发 Haskell。收录于:第二十三届 ACM 编程语言原理研讨会论文集;1996:295–308 圣彼得堡海滩,佛罗里达州,一月。
[JGF96] Jones Simon Peyton, Gordon Andrew, Finne Sigbjorn. Concurrent Haskell. In: Proceedings of the Twenty-Third ACM Symposium on Principles of Programming Languages; 1996:295–308 St. Petersburg Beach, FL, January.
[JL96]Jones Richard、Lins Rafael。垃圾收集:自动动态内存管理算法。纽约:John Wiley and Sons;1996 年。
[JL96] Jones Richard, Lins Rafael. Garbage Collection: Algorithms for Automatic Dynamic Memory Management. New York, NY: John Wiley and Sons; 1996.
[JM94]Jaffar Joxan,Maher Michael J. 约束逻辑编程:调查。《逻辑编程杂志》。1994;20:503–581 五月至七月。
[JM94] Jaffar Joxan, Maher Michael J. Constraint logic programming: A survey. Journal of Logic Programming. 1994;20:503–581 May–July.
[约翰福音75]Johnson Stephen C. Yacc—又一个编译器。新泽西州 Murray Hill:AT&T Bell 实验室;1975 年技术报告 32,计算机科学。
[Joh75] Johnson Stephen C. Yacc—Yet another compiler compiler. Murray Hill, NJ: AT&T Bell Laboratories; 1975 Technical Report 32, Computing Science.
[JOR75]Jazayeri Mehdi、Ogden William F.、Rounds William C. 属性语法循环问题的内在指数复杂度。ACM通讯。1975年 12 月 18(12):697-706。
[JOR75] Jazayeri Mehdi, Ogden William F., Rounds William C. The intrinsically exponential complexity of the circularity problem for attribute grammars. Communications of the ACM. 1975;18(12):697–706 December.
[JPAR68]Johnson Walter L.,Porter James H.,Ackley Stephanie I.,Ross Douglas T. 使用有限状态技术自动生成高效词汇处理器。ACM通讯。1968;11(12):805-813 十二月。
[JPAR68] Johnson Walter L., Porter James H., Ackley Stephanie I., Ross Douglas T. Automatic generation of efficient lexical processors using finite state techniques. Communications of the ACM. 1968;11(12):805–813 December.
[JW91]Jensen Kathleen、Wirth Niklaus。Pascal用户手册和报告:ISO Pascal 标准。第四版纽约:Springer-Verlag;1991 年由 Andrew B. Mickel 和 James F. Miner 修订。ISBN 0-387-97649-3。
[JW91] Jensen Kathleen, Wirth Niklaus. Pascal User Manual and Report: ISO Pascal Standard. fourth edition New York, NY: Springer-Verlag; 1991 Revised by Andrew B. Mickel and James F. Miner. ISBN 0-387-97649-3.
[卡斯65]Kasami T.一种高效的上下文无关语言识别和语法分析算法。马萨诸塞州贝德福德:空军剑桥研究实验室;1965 年技术报告 AFCRL-65-758。
[Kas65] Kasami T. An efficient recognition and syntax analysis algorithm for context-free languages. Bedford, MA: Air Force Cambridge Research Laboratory; 1965 Technical Report AFCRL-65-758.
[九铁+ 98]Kelsey Richard、Clinger William、Rees Jonathan、Abelson H.、Dybvig RK、Haynes CT、Rozas GJ、Adams NI IV、Friedman DP、Kohlbecker E.、Steele GL Jr.、Bartley DH、Halstead R.、Oxley D.、Sussman GJ、Brooks G.、Hanson C.、Pitman KM、Wand M.算法语言 Scheme 的修订报告5。高阶与符号计算。1998年。;11(1):7–105。由 Kelsey、Clinger 和 Rees 编辑。可在schemers.org/Documents/Standards/R5RS/上获取。
[KCR+98] Kelsey Richard, Clinger William, Rees Jonathan, Abelson H., Dybvig R.K., Haynes C.T., Rozas G.J., Adams N.I. IV, Friedman D.P., Kohlbecker E., Steele G.L. Jr., Bartley D.H., Halstead R., Oxley D., Sussman G.J., Brooks G., Hanson C., Pitman K.M., Wand M. Revised5 report on the algorithmic language Scheme. Higher-Order and Symbolic Computation. 1998. ;11(1):7–105. Edited by Kelsey, Clinger, and Rees. Available at schemers.org/Documents/Standards/R5RS/.
[Kee89]Keene Sonya E. Common Lisp 中的面向对象编程:CLOS 程序员指南。马萨诸塞州雷丁:Addison-Wesley;1989 年 Dan Gerson 供稿。
[Kee89] Keene Sonya E. Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS. Reading, MA: Addison-Wesley; 1989 Contributions by Dan Gerson.
[Kep04]Kepser Stephan。XSLT和 XQuery 图灵完备性的简单证明。收录于:2004 年极端标记语言会议论文集,加拿大蒙特利尔;2004 年 8 月可从conferences.idealliance.org/extreme/html/2004/Kepser01/EML2004Kepser01.html获取。
[Kep04] Kepser Stephan. A simple proof for the Turing-completeness of XSLT and XQuery. In: Proceedings, Extreme Markup Languages 2004, Montréal, Canada; 2004. August Available as conferences.idealliance.org/extreme/html/2004/Kepser01/EML2004Kepser01.html.
[Ker81]Kernighan Brian W.为什么 Pascal 不是我最喜欢的编程语言。Murray Hill,新泽西州:计算机科学,AT&T 贝尔实验室;1981 年技术报告 100 重印为 Feuer 和 Gehani [FG84] 的第 170-186 页。
[Ker81] Kernighan Brian W. Why Pascal is not my favorite programming language. Murray Hill, NJ: Computing Science, AT&T Bell Laboratories; 1981 Technical Report 100 Reprinted as pages 170–186 of Feuer and Gehani [FG84].
[77 卡斯]Kessels Joep LW 监视器中用于同步的事件队列的替代方案。《ACM 通讯》。1977;20(7):7 月 500-503。
[Kes77] Kessels Joep L.W. An alternative to event queues for synchronization in monitors. Communications of the ACM. 1977;20(7):500–503 July.
[克莱56]Kleene Stephen C. 神经网络和有限自动机中的事件表示。收录于:Shannon Claude E.、McCarthy John 编。《自动机研究》。新泽西州普林斯顿:普林斯顿大学出版社;1956 年:3-41,《数学研究年鉴》第 34 期。
[Kle56] Kleene Stephen C. Representation ofevents in nerve nets and finite automata. In: Shannon Claude E., McCarthy John, eds. Automata Studies. Princeton, NJ: Princeton University Press; 1956:3–41 number 34 in Annals of Mathematical Studies.
[KMP77]Knuth Donald E.、Morris James H.、Pratt Vaughan R. 字符串中的快速模式匹配。SIAM计算杂志。1977;6(2):6 月 323-350。
[KMP77] Knuth Donald E., Morris James H., Pratt Vaughan R. Fast pattern matching in strings. SIAM Journal of Computing. 1977;6(2):323–350 June.
[Knu65]Knuth Donald E. 论从左到右的语言翻译。信息与控制。1965;8(6):607–639 十二月。
[Knu65] Knuth Donald E. On the translation of languages from left to right. Information and Control. 1965;8(6):607–639 December.
[Knu68]Knuth Donald E. 上下文无关语言的语义。数学系统理论。1968;2(2):127–145 六月更正见第 5 卷第 95–96 页。
[Knu68] Knuth Donald E. Semantics of context-free languages. Mathematical Systems Theory. 1968;2(2):127–145 June Correction appears in Volume 5, pages 95–96.
[Knu84]Knuth Donald E. 文学编程。计算机杂志。1984;27(2):97-111 月。
[Knu84] Knuth Donald E. Literate programming. The Computer Journal. 1984;27(2):97–111 May.
[Knu86]Knuth Donald E. The TeXbook。马萨诸塞州雷丁:Addison-Wesley;1986 年。
[Knu86] Knuth Donald E. The TeXbook. Reading, MA: Addison-Wesley; 1986.
[Kor94]Korn David G. ksh:一种可扩展的高级语言。摘自:USENIX 超高级语言研讨会论文集;1994:129–146 新墨西哥州圣达菲,十月。
[Kor94] Korn David G. ksh: An extensible high level language. In: Proceedings of the USENIX Very High Level Languages Symposium; 1994:129–146 Santa Fe, NM, October.
[第 78 页]Kernighan Brian W.、Plauger Phillip J.《编程风格的要素》。第二版,纽约:McGraw-Hill;1978 年。
[KP78] Kernighan Brian W., Plauger Phillip J. The Elements of Programming Style. second edition New York, NY: McGraw-Hill; 1978.
[KR88]Kernighan Brian W.、Ritchie Dennis M. 《C 编程语言》。第二版 Englewood Cliffs,新泽西州:Prentice-Hall;1988 年。
[KR88] Kernighan Brian W., Ritchie Dennis M. The C Programming Language. second edition Englewood Cliffs, NJ: Prentice-Hall; 1988.
[克拉73]Král Jaroslav。模式的等价性和有限自动机的等价性。ALGOL Bulletin。1973;35:34-35 月。
[Krá73] Král Jaroslav. The equivalence of modes and the equivalence of finite automata. ALGOL Bulletin. 1973;35:34–35 March.
[KS01]Kennedy Andrew、Syme Don。.NET通用语言运行时的泛型设计和实现。摘自:SIGPLAN 2001 编程语言设计和实现会议论文集;2001 年 6 月,犹他州 Snowbird,1-12。
[KS01] Kennedy Andrew, Syme Don. Design and implementation of generics for the .NET Common Language Runtime. In: Proceedings of the SIGPLAN 2001 Conference on Programming Language Design and Implementation; 2001:1–12 Snowbird, UT, June.
[KWS97]Kontothanassis Leonidas I.、Wisniewski Robert、Scott Michael L. 调度程序意识同步。ACM计算机系统学报。1997;15(1):2 月 3 日到 40 日。
[KWS97] Kontothanassis Leonidas I., Wisniewski Robert, Scott Michael L. Scheduler-conscious synchronization. ACM Transactions on Computer Systems. 1997;15(1):3–40 February.
[Lam78]Lamport Leslie。分布式系统中的时间、时钟和事件排序。ACM通讯。1978;21(7):7 月 558-565。
[Lam78] Lamport Leslie. Time, clocks, and the ordering of events in a distributed system. Communications of the ACM. 1978;21(7):558–565 July.
[Lam87]Lamport Leslie。一种快速互斥算法。ACM计算机系统学报。1987;5(1):2 月 1 日 - 11 日。
[Lam87] Lamport Leslie. A fast mutual exclusion algorithm. ACM Transactions on Computer Systems. 1987;5(1):1–11 February.
[Lam94]Lamport Leslie。LaTeX :文档准备系统。第二版,马萨诸塞州雷丁:Addison-Wesley Professional;1994 年。
[Lam94] Lamport Leslie. LaTeX: A Document Preparation System. second edition Reading, MA: Addison-Wesley Professional; 1994.
[Lam05]Lameter Christoph。Linux /NUMA 系统上的有效同步。收录于:Gelato 联盟会议论文集,加利福尼亚州圣何塞;2005 年 5 月。
[Lam05] Lameter Christoph. Effective synchronization on Linux/NUMA systems. In: In Proceedings of the Gelato Federation Meeting, San Jose, CA; 2005 May.
[液晶显示模组+05 ]Luk Chi-Keung、Cohn Robert、Muth Robert、Patil Harish、Klauser Artur、Lowney Geoff、Wallace Steven、Reddi Vijay Janapa、Hazelwood Kim。Pin :使用动态检测构建自定义程序分析工具。在:SIGPLAN 2005 编程语言设计和实现会议论文集;2005:190-200 芝加哥,伊利诺伊州,六月。
[LCM+05] Luk Chi-Keung, Cohn Robert, Muth Robert, Patil Harish, Klauser Artur, Lowney Geoff, Wallace Steven, Reddi Vijay Janapa, Hazelwood Kim. Pin: Building customized program analysis tools with dynamic instrumentation. In: Proceedings of the SIGPLAN 2005 Conference on Programming Language Design and Implementation; 2005:190–200 Chicago, IL, June.
[Lee06]Lee Edward A. 线程问题。计算机。2006;39(5):5 月 33-42 日。
[Lee06] Lee Edward A. The problem with threads. Computer. 2006;39(5):33–42 May.
[莱斯75]Lesk Michael E. Lex—词汇分析器生成器。新泽西州 Murray Hill:计算机科学,AT&T 贝尔实验室;1975 年技术报告 39。
[Les75] Lesk Michael E. Lex—A lexical analyzer generator. Murray Hill, NJ: Computing Science, AT&T Bell Laboratories; 1975 Technical Report 39.
[LG86]Barbara Liskov,John Guttag。《程序开发中的抽象与规范》。马萨诸塞州剑桥:麻省理工学院出版社;1986 年。
[LG86] Liskov Barbara, Guttag John. Abstraction and Specification in Program Development. Cambridge, MA: MIT Press; 1986.
[LH89]Li Kai, Hudak Paul。共享虚拟内存系统中的内存一致性。ACM计算机系统学报。1989;7(4):321-359 年 11 月。
[LH89] Li Kai, Hudak Paul. Memory coherence in shared virtual memory systems. ACM Transactions on Computer Systems. 1989;7(4):321–359 November.
[左手边+ 77]Lampson Butler W.、Horning JJ、London RL、Mitchell JG、Popek GJ 关于编程语言 Euclid 的报告。ACM SIGPLAN 通知。1977;12(2):2 月 1-79 日。
[LHL+77] Lampson Butler W., Horning J.J., London R.L., Mitchell J.G., Popek G.J. Report on the programming language Euclid. ACM SIGPLAN Notices. 1977;12(2):1–79 February.
[LK08]Larus James,Kozyrakis Christos。事务性内存。ACM通讯。2008;51(7):7 月 80-88 日。
[LK08] Larus James, Kozyrakis Christos. Transactional memory. Communications of the ACM. 2008;51(7):80–88 July.
[LL12]Louden Kenneth C.、Lambert Kenneth A.编程语言:原理与实践。第三版,马萨诸塞州波士顿:Cengage Learning;2012 年。
[LL12] Louden Kenneth C., Lambert Kenneth A. Programming Languages: Principles and Practice. third edition Boston, MA: Cengage Learning; 2012.
[Llo87]Lloyd John W. 《逻辑编程基础》。第二版柏林,西德:Springer-Verlag;1987 年。
[Llo87] Lloyd John W. Foundations of Logic Programming. second edition Berlin, West Germany: Springer-Verlag; 1987.
[洛姆75]Lomet David B. 使对释放存储的引用无效的方案。IBM研究与开发杂志。1975;19(1):1 月 26-35 日。
[Lom75] Lomet David B. Scheme for invalidating references to freed storage. IBM Journal of Research and Development. 1975;19(1):26–35 January.
[Lom85]Lomet David B. 在系统编程语言中确保指针安全。IEEE软件工程学报。1985;SE–11(1):87–96 年 1 月。
[Lom85] Lomet David B. Making pointers safe in system programming languages. IEEE Transactions on Software Engineering. 1985;SE–11(1):87–96 January.
[LP80]Luckam David C.,Polak W. Ada 异常处理:一种公理方法。ACM编程语言和系统事务。1980;2(2):4 月 225-233。
[LP80] Luckam David C., Polak W. Ada exception handling: An axiomatic approach. ACM Transactions on Programming Languages and Systems. 1980;2(2):225–233 April.
[LR80]Lampson Butler W.,Redell David D. Mesa 中的进程和监视器经验。ACM通讯。1980;23(2):2 月 105-117。
[LR80] Lampson Butler W., Redell David D. Experience with processes and monitors in Mesa. Communications of the ACM. 1980;23(2):105–117 February.
[LRS74]Lewis Philip M. II、Rosenkrantz Daniel J.、Stearns Richard E. 署名翻译。《计算机与系统科学杂志》。1974;9(3):279–307 年 12 月。
[LRS74] Lewis Philip M. II, Rosenkrantz Daniel J., Stearns Richard E. Attributed translations. Journal of Computer and System Sciences. 1974;9(3):279–307 December.
[LS68]Lewis Philip M. II、Stearns Richard E. 句法导向转导。《ACM 杂志》。1968;15(3):465–488 年 7 月。
[LS68] Lewis Philip M. II, Stearns Richard E. Syntax-directed transduction. Journal of the ACM. 1968;15(3):465–488 July.
[LS79]Barbara Liskov,Alan Snyder。CLU 中的异常处理。IEEE软件工程学报。1979;SE–5(6):546–558 十一月。
[LS79] Liskov Barbara, Snyder Alan. Exception handling in CLU. IEEE Transactions on Software Engineering. 1979;SE–5(6):546–558 November.
[LS83]Barbara Liskov,Robert Scheifler。《守护者与行动:对稳健分布式程序的语言支持》。《ACM 编程语言与系统汇刊》。1983;5(3):7 月 381-404。
[LS83] Liskov Barbara, Scheifler Robert. Guardians and actions: Linguistic support for robust, distributed programs. ACM Transactions on Programming Languages and Systems. 1983;5(3):381–404 July.
[LSAS77]Barbara Liskov、Alan Snyder、Atkinson Russel、Craig Schaffert J. CLU 中的抽象机制。ACM通讯。1977;20(8):8 月 564-576 日。
[LSAS77] Liskov Barbara, Snyder Alan, Atkinson Russel, Craig Schaffert J. Abstraction mechanisms in CLU. Communications of the ACM. 1977;20(8):564–576 August.
[LYBB14]Lindholm Tim、Yellin Frank、Bracha Gilad、Buckley Alex。Java虚拟机规范。Java SE 8 版 Addison-Wesley Professional;2014 年。可在docs.oracle.com/javase/specs/上获取。
[LYBB14] Lindholm Tim, Yellin Frank, Bracha Gilad, Buckley Alex. The Java Virtual Machine Specification. Java SE 8 edition Addison-Wesley Professional; 2014. Available at docs.oracle.com/javase/specs/.
[Mac77]Donald MacLaren M. PL/I 中的异常处理。收录于:Wortman David B. 编辑。ACM 可靠软件语言设计会议论文集;1977 年:101–104 北卡罗来纳州罗利市。
[Mac77] Donald MacLaren M. Exception handling in PL/I. In: Wortman David B., ed. Proceedings of an ACM Conference on Language Design for Reliable Software; 1977:101–104 Raleigh, NC.
[MAE + 65]McCarthy John、Abrahams Paul W.、Edwards Daniel J.、Hart Timothy P.、Levin Michael I. LISP 1.5 程序员手册。第二版马萨诸塞州剑桥:麻省理工学院出版社;1965 年可作为 softwarepreservation.org/projects/LISP/book/LISP%201.5%20Programmers%20Manual.pdf 获得。
[MAE+65] McCarthy John, Abrahams Paul W., Edwards Daniel J., Hart Timothy P., Levin Michael I. LISP 1.5 Programmer's Manual. second edition Cambridge, MA: MIT Press; 1965 Available as softwarepreservation.org/projects/LISP/book/LISP%201.5%20Programmers%20Manual.pdf.
[麦90]Mairson Harry G.确定 ML 可类型化性在确定性指数时间内完成。在:第十七届 ACM 编程语言原理研讨会会议记录;1990:382–401 旧金山,加利福尼亚州,一月。
[Mai90] Mairson Harry G. Deciding ML typability is complete for deterministic exponential time. In: Conference Record of the Seventeenth Annual ACM Symposium on Principles of Programming Languages; 1990:382–401 San Francisco, CA, January.
[MAK + 01]麦肯尼·保罗·E.、阿帕沃·乔纳森、克莱恩·安迪、克里格·奥兰、拉塞尔·鲁斯蒂、萨尔玛·迪潘卡、索尼·曼尼什。读取复制更新。见:渥太华 Linux 研讨会论文集; 2001:338–367 加拿大安大略省渥太华,七月。
[MAK+01] McKenney Paul E., Appavoo Jonathan, Kleen Andi, Krieger Orran, Russel Rusty, Sarma Dipankar, Soni Maneesh. Read-copy update. In: Proceedings of the Ottawa Linux Symposium; 2001:338–367 Ottawa, ON, Canada, July.
[马斯76]Mashey John R.使用命令语言作为高级编程语言。引自:第二届国际 IEEE 软件工程大会论文集;1976:169–176 旧金山,加利福尼亚州,十月。
[Mas76] Mashey John R. Using a command language as a high-level programming language. In: Proceedings of the Second International IEEE Conference on Software Engineering; 1976:169–176 San Francisco, CA, October.
[马斯87]Massalin Henry。超级优化器:看看最小的程序。在:第二届编程语言和操作系统架构支持国际会议论文集;1987:122-126 帕洛阿尔托,加利福尼亚州,十月。
[Mas87] Massalin Henry. Superoptimizer: A look at the smallest program. In: Proceedings of the Second International Conference on Architectural Support for Programming Languages and Operating Systems; 1987:122–126 Palo Alto, CA, October.
[McC60]McCarthy John。符号表达式的递归函数及其机器计算,第一部分。ACM通讯。1960;3(4):4 月 184-195 日。
[McC60] McCarthy John. Recursive functions of symbolic expressions and their computation by machine, Part I. Communications of the ACM. 1960;3(4):184–195 April.
[麦克格82]McGraw James R. VAL 语言:描述和分析。ACM编程语言和系统事务。1982;4(1):44-82 年 1 月。
[McG82] McGraw James R. The VAL language: Description and analysis. ACM Transactions on Programming Languages and Systems. 1982;4(1):44–82 January.
[麦克K04]McKinley Kathryn S. 编辑。ACM SIGPLAN 编程语言设计和实现会议 20 周年,1979-1999。纽约:ACM Press;2004 年,另见 ACM SIGPLAN 通知,39(4),2004 年 4 月。
[McK04] McKinley Kathryn S., ed. 20 Years of the ACM SIGPLAN Conference on Programming Language Design and Implementation, 1979–1999. New York, NY: ACM Press; 2004 Also ACM SIGPLAN Notices, 39(4), April 2004.
[MCS91]Mellor-Crummey John M.,Scott Michael L. 共享内存多处理器上的可扩展同步算法。ACM计算机系统学报。1991;9(1):2 月 21-65 日。
[MCS91] Mellor-Crummey John M., Scott Michael L. Algorithms for scalable synchronization on shared-memory multiprocessors. ACM Transactions on Computer Systems. 1991;9(1):21–65 February.
[Mes12]消息传递接口论坛。MPI :消息传递接口标准。2012年 9 月,版本 3.0。可从mpi-forum.org/docs/mpi-3.0/mpi30-report.pdf获取。
[Mes12] Message Passing Interface Forum. MPI: A Message-Passing Interface Standard. 2012. September Version 3.0. Available as mpi-forum.org/docs/mpi-3.0/mpi30-report.pdf.
[Mey92a]Meyer Bertrand。应用“契约式设计”。IEEE计算机。1992;25(10):10 月 40-51 日。
[Mey92a] Meyer Bertrand. Applying "design by contract". IEEE Computer. 1992;25(10):40–51 October.
[Mey92b]Meyer Bertrand。《埃菲尔:语言》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1992 年。
[Mey92b] Meyer Bertrand. Eiffel: The Language. Englewood Cliffs, NJ: Prentice-Hall; 1992.
[MF08]Matthews Jacob,Findel Robert Bruce。Scheme 的操作语义。函数式编程杂志。2008;18(1):47-86。
[MF08] Matthews Jacob, Findler Robert Bruce. An operational semantics for Scheme. Journal of Functional Programming. 2008;18(1):47–86.
[MGA92]Martonosi Margaret、Gupta Anoop、Anderson Thomas。MemSpy :分析程序中的内存系统瓶颈。收录于:1992 年 ACM SIGMETRICS 计算机系统测量与建模联合国际会议论文集;1992 年:1-12 纽波特,罗德岛州,六月。
[MGA92] Martonosi Margaret, Gupta Anoop, Anderson Thomas. MemSpy: Analyzing memory system bottlenecks in programs. In: Proceedings of the 1992 ACM SIGMETRICS Joint International Conference on Measurement and Modeling of Computer Systems; 1992:1–12 Newport, RI, June.
[麦克风68]Michie Donald。“备忘录”功能和机器学习。《自然》。1968;218(5136):4 月 19-22 日。
[Mic68] Michie Donald. ‘Memo’ functions and machine learning. Nature. 1968;218(5136):19–22 April.
[麦克风89]Michaelson Greg。通过 Lambda 演算介绍函数式编程。英国沃金厄姆:Addison-Wesley;1989 年国际计算机科学系列。
[Mic89] Michaelson Greg. An Introduction to Functional Programming through Lambda Calculus. Wokingham, England: Addison-Wesley; 1989 International Computer Science Series.
[麦克风12]微软公司。C # 语言规范。2012年。版本 5.0 可在msdn.microsoft.com/en-us/library/ms228593.aspx上获取。
[Mic12] Microsoft Corporation. C# Language Specification. 2012. Version 5.0 Available at msdn.microsoft.com/en-us/library/ms228593.aspx.
[米尔78]Milner Robin。编程中的类型多态性理论。计算机与系统科学杂志。1978;17(3):12 月 348-375。
[Mil78] Milner Robin. A theory of type polymorphism in programming. Journal of Computer and System Sciences. 1978;17(3):348–375 December.
[MKH91]Mohr Eric、Kranz David A.、Jr Robert H. Halstead。惰性任务创建:一种提高并行程序粒度的技术。IEEE并行和分布式系统学报。1991;2(3):7 月 264-280。
[MKH91] Mohr Eric, Kranz David A., Jr Robert H. Halstead. Lazy task creation: A technique for increasing the granularity of parallel programs. IEEE Transactions on Parallel and Distributed Systems. 1991;2(3):264–280 July.
[美国职棒大联盟76]Marcotty Michael、Ledgard Henry F.、Bochmann Gregor V. 正式定义样本。ACM计算调查。1976;8(2):191-276 年 6 月。
[MLB76] Marcotty Michael, Ledgard Henry F., Bochmann Gregor V. A sampler of formal definitions. ACM Computing Surveys. 1976;8(2):191–276 June.
[MM08]Marathe Virendra J.,Moir Mark。迈向高性能非阻塞软件事务内存。收录于:第十三届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2008 年:227–236 犹他州盐湖城,二月。
[MM08] Marathe Virendra J., Moir Mark. Toward high performance nonblocking software transactional memory. In: Proceedings of the Thirteenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2008:227–236 Salt Lake City, UT, February.
[Moo78]Moon David A. MacLisp 参考手册。麻省理工学院人工智能实验室;1978 年。
[Moo78] Moon David A. MacLisp Reference Manual. MIT Artificial Intelligence Laboratory; 1978.
[Moo86]Moon David A.具有 Flavors 的面向对象编程。引自:OOPSLA '86 会议论文集:面向对象编程系统、语言和应用程序;1986 年 9 月,波特兰,1-8。
[Moo86] Moon David A. Object-oriented programming with Flavors. In: OOPSLA '86 Conference Proceedings: Object-Oriented Programming Systems, Languages, and Applications; 1986:1–8 Portland, OR, September.
[Mor70]Morgan Howard L. 系统程序中的拼写更正。《ACM 通讯》。1970;13(2):90–94。
[Mor70] Morgan Howard L. Spelling correction in systems programs. Communications of the ACM. 1970;13(2):90–94.
[MOSS96]Murer Stephan、Omohundro Stephen、Stoutamire David、Szyperski Clemens。Sather 中的迭代抽象。ACM编程语言和系统事务。1996;18(1):1 月 1 日 - 15 日。
[MOSS96] Murer Stephan, Omohundro Stephen, Stoutamire David, Szyperski Clemens. Iteration abstraction in Sather. ACM Transactions on Programming Languages and Systems. 1996;18(1):1–15 January.
[MPA05]Manson Jeremy、Pugh William、Adve Sarita V. Java 内存模型。摘自:第三十二届 ACM 编程语言原理研讨会论文集;2005:378–391 加州长滩,1 月。
[MPA05] Manson Jeremy, Pugh William, Adve Sarita V. The Java memory model. In: Proceedings of the Thirty-Second ACM Symposium on Principles of Programming Languages; 2005:378–391 Long Beach, CA, January.
[MR96]Metcalf Michael,Reid John。《Fortran 90/95 解析》。英国伦敦:牛津大学出版社;1996 年。
[MR96] Metcalf Michael, Reid John. Fortran 90/95 Explained. London, England: Oxford University Press; 1996.
[MR04]Miller James S.、Ragsdale Susann。《通用语言基础结构注释标准》。马萨诸塞州波士顿:Addison-Wesley;2004 年基于 ECMA-335,第 2 版,2002 年。
[MR04] Miller James S., Ragsdale Susann. The Common Language Infrastructure Annotated Standard. Boston, MA: Addison-Wesley; 2004 Based on ECMA-335, 2nd Edition, 2002.
[MS96]Michael Maged M.,Scott Michael L.简单、快速、实用的非阻塞和阻塞并发队列算法。摘自:第十五届 ACM 分布式计算原理研讨会论文集;1996:267–275 宾夕法尼亚州费城,五月。
[MS96] Michael Maged M., Scott Michael L. Simple, fast, and practical non-blocking and blocking concurrent queue algorithms. In: Proceedings of the Fifteenth Annual ACM Symposium on Principles of Distributed Computing; 1996:267–275 Philadelphia, PA, May.
[MS98]Michael Maged M.,Scott Michael L. 多道程序共享内存多处理器上的非阻塞算法和抢占安全锁定。并行和分布式计算杂志。1998;51:1-26。
[MS98] Michael Maged M., Scott Michael L. Nonblocking algorithms and preemption-safe locking on multiprogrammed shared memory multiprocessors. Journal of Parallel and Distributed Computing. 1998;51:1–26.
[MTAB13]Meyerovich Leo A.、Torok Matthew E.、Atkinson Eric、Bodik Rastislav。属性语法的并行调度合成。收录于:第十八届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2013 年 2 月,中国深圳,187–196。
[MTAB13] Meyerovich Leo A., Torok Matthew E., Atkinson Eric, Bodik Rastislav. Parallel schedule synthesis for attribute grammars. In: Proceedings of the Eighteenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2013:187–196 Shenzhen, China, February.
[MTHM97]Milner Robin、Tofte Mads、Harper Robert 和 David MacQueen。《标准 ML 的定义——修订版》。马萨诸塞州剑桥:麻省理工学院出版社;1997 年。
[MTHM97] Milner Robin, Tofte Mads, Harper Robert, MacQueen David. The Definition of Standard ML—Revised. Cambridge, MA: MIT Press; 1997.
[Muc97]Muchnick Steven S.高级编译器设计和实现。旧金山,加州:Morgan Kaufmann;1997 年。
[Muc97] Muchnick Steven S. Advanced Compiler Design and Implementation. San Francisco, CA: Morgan Kaufmann; 1997.
[95 马币]McKenzie Bruce J.、Yeatman Corey、De Vere Lorraine。《移位归约解析器中的错误修复》。《ACM 编程语言和系统汇刊》。1995;17(4):7 月 672-689 日。
[MYD95] McKenzie Bruce J., Yeatman Corey, De Vere Lorraine. Error repair in shift-reduce parsers. ACM Transactions on Programming Languages and Systems. 1995;17(4):672–689 July.
[NA01]尼基尔·里希尤尔·S.,阿尔文德。pH 中的隐式并行编程。加利福尼亚州旧金山:摩根·考夫曼; 2001年。
[NA01] Nikhil Rishiyur S., Arvind. Implicit Parallel Programming in pH. San Francisco, CA: Morgan Kaufmann; 2001.
[NBB + 63]Peter Naur (ed.) Backus JW、Bauer FL、Green J.、Katz C.、McCarthy J.、Perlis AJ、Rutishauser H.、Samelson K.、Vauquois B.、Wegstein JH、van Wijngaarden A. 和 Woodger M. 算法语言 ALGOL 60 的修订报告。ACM通讯。1963;6(1):1-1 月 23 日 原始版本出现在 1960 年 5 月刊中。
[NBB+63] Peter Naur (ed.) Backus J.W., Bauer F.L., Green J., Katz C., McCarthy J., Perlis A.J., Rutishauser H., Samelson K., Vauquois B., Wegstein J.H., van Wijngaarden A., Woodger M. Revised report on the algorithmic language ALGOL 60. Communications of the ACM. 1963;6(1):1–23 January Original version appeared in the May 1960 issue.
[ND78] Kristen Nygaard 和 Ole-Johan Dahl。《Simula 语言的发展》。《HOPL I 论文集》[Wex78],第 439-493 页。
[ND78] Kristen Nygaard and Ole-Johan Dahl. The development of the Simula languages. In HOPL I Proceedings [Wex78], pages 439-493.
[Nec97]Necula George C.带有证明的代码。出处:第二十四届 ACM 编程语言原理研讨会会议记录;1997 年 1 月,法国巴黎,第 106-119 页。
[Nec97] Necula George C. Proof-carrying code. In: Conference Record of the Twenty-Fourth ACM Symposium on Principles of Programming Languages; 1997:106–119 Paris, France, January.
[Nel65]纳尔逊·西奥多·霍尔姆。复杂信息处理:复杂、变化和不确定的文件结构。收录于:第二十届 ACM 全国大会论文集;1965 年 84-100 页,俄亥俄州克利夫兰,八月。
[Nel65] Nelson Theodor Holm. Complex information processing: A file structure for the complex, the changing, and the indeterminate. In: Proceedings of the Twentieth ACM National Conference; 1965 pages 84-100, Cleveland, OH, August.
[NK15]Nipkow Tobias、Klein Gerwin。《Isabelle/HOL 的具体语义学》。德国柏林:Springer Science+Business Media;2015 年。
[NK15] Nipkow Tobias, Klein Gerwin. Concrete Semantics with Isabelle/HOL. Berlin, Germany: Springer Science+Business Media; 2015.
[Ous82]Ousterhout John K.并发系统的调度技术。在:第三届国际分布式计算系统会议论文集;1982:22-30 迈阿密/劳德代尔堡,佛罗里达州,十月。
[Ous82] Ousterhout John K. Scheduling techniques for concurrent systems. In: Proceedings of the Third International Conference on Distributed Computing Systems; 1982:22–30 Miami/Ft. Lauderdale, FL, October.
[Ous94]Ousterhout John K. Tcl 和 Tk 工具包。马萨诸塞州雷丁:Addison-Wesley Professional;1994 年。
[Ous94] Ousterhout John K. Tcl and the Tk Toolkit. Reading, MA: Addison-Wesley Professional; 1994.
[Ous98]Ousterhout John K. 脚本:面向 21 世纪的高级编程。IEEE计算机。1998;31(3):3 月 23-30 日。
[Ous98] Ousterhout John K. Scripting: Higher-level programming for the 21st century. IEEE Computer. 1998;31(3):23–30 March.
[第76页]Pagan Frank G. 《Algol 68 实用指南》。英国伦敦:John Wiley and Sons;1976 年。
[Pag76] Pagan Frank G. A Practical Guide to Algol 68. London, England: John Wiley and Sons; 1976.
[标准杆72]Parnas David L. 论将系统分解为模块时应使用的标准。ACM通讯。1972;15(12):1053-1058 年 12 月。
[Par72] Parnas David L. On the criteria to be used in decomposing systems into modules. Communications of the ACM. 1972;15(12):1053–1058 December.
[Pat85]Patterson David A. 精简指令集计算机。《ACM 通讯》。1985;28(1):1 月 8-21 日。
[Pat85] Patterson David A. Reduced instruction set computers. Communications of the ACM. 1985;28(1):8–21 January.
[PD80]Patterson David A.,Ditzel David R. 精简指令集计算机的案例。ACM SIGARCH 计算机架构新闻。1980;8(6):10 月 25-33 日。
[PD80] Patterson David A., Ditzel David R. The case for the reduced instruction set computer. ACM SIGARCH Computer Architecture News. 1980;8(6):25–33 October.
[PD12]Peterson Larry L.、Davie Bruce S.计算机网络:系统方法。第五版旧金山,加利福尼亚州:Morgan Kaufmann;2012 年。
[PD12] Peterson Larry L., Davie Bruce S. Computer Networks: A Systems Approach. fifth edition San Francisco, CA: Morgan Kaufmann; 2012.
[宠物81]Peterson Gary L. 关于互斥问题的误解。信息处理快报。1981;12(3):6 月 115-116 日。
[Pet81] Peterson Gary L. Myths about the mutual exclusion problem. Information Processing Letters. 1981;12(3):115–116 June.
[Pey87]Peyton Jones Simon L.函数式编程语言的实现。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1987 年。
[Pey87] Peyton Jones Simon L. The Implementation of Functional ProgrammingLanguages. Englewood Cliffs, NJ: Prentice-Hall; 1987.
[Pey92]Peyton Jones Simon L. 在普通硬件上实现惰性函数式语言:Spineless Tagless G-machine。《函数式编程杂志》。1992;2(2):127–202。
[Pey92] Peyton Jones Simon L. Implementing lazy functional languages on stock hardware: The Spineless Tagless G-machine. Journal of Functional Programming. 1992;2(2):127–202.
[Pey01]Peyton Jones Simon。《解决尴尬小队:Haskell 中的单子输入/输出、并发、异常和外语调用》。收录于:Hoare Tony、Broy Manfred、Steinbruggen Ralf 编。《软件构建的工程理论》。IOS Press;2001:47-96。最初在2000 年Marktoberdorf 暑期学校发表。修订和更正版本可从research.microsoft.com/~simonpj/papers/marktoberdorf/mark.pdf获取。
[Pey01] Peyton Jones Simon. Tackling the Awkward Squad: Monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell. In: Hoare Tony, Broy Manfred, Steinbruggen Ralf, eds. Engineering Theories of Software Construction. IOS Press; 2001:47–96. Originally presented at the Marktoberdorf Summer School, 2000. Revised and corrected version available as research.microsoft.com/~simonpj/papers/marktoberdorf/mark.pdf.
[PH12]Patterson David A.、Hennessy John L.计算机组织和设计:硬件-软件接口。第四版,修订版旧金山,加利福尼亚州:Morgan Kaufmann;2012 年。
[PH12] Patterson David A., Hennessy John L. Computer Organization and Design: The Hardware-Software Interface. fourth, revised edition San Francisco, CA: Morgan Kaufmann; 2012.
[馅饼02]Pierce Benjamin C.类型和编程语言。马萨诸塞州剑桥:麻省理工学院出版社;2002 年。
[Pie02] Pierce Benjamin C. Types and Programming Languages. Cambridge, MA: MIT Press; 2002.
[PJ10]Haskell 2010 语言报告。2010年 4 月可在haskell.org/onlinereport/haskell2010/上查阅。
[PJ10] Haskell 2010 Language Report. 2010. April Available at haskell.org/onlinereport/haskell2010/.
[PQ95]Parr Terrence J.,Quong RW ANTLR:一种谓词型LL ( k ) 解析器生成器。软件——实践与经验。1995;25(7):789-810 七月。
[PQ95] Parr Terrence J., Quong R.W. ANTLR: A predicated-LL(k) parser generator. Software—Practice and Experience. 1995;25(7):789–810 July.
[哈巴狗00]Pugh William。Java 内存模型存在致命缺陷。并发性——实践与经验。2000年 5 月 12(6):445-455。
[Pug00] Pugh William. The Java memory model is fatally flawed. Concurrency—Practice and Experience. 2000;12(6):445–455 May.
[Rad82]Radin George。801小型计算机。引自:第一届编程语言和操作系统架构支持国际研讨会论文集;1982 年 3 月,加利福尼亚州帕洛阿尔托,第 39-47 页。
[Rad82] Radin George. The 801 minicomputer. In: Proceedings of the First International Symposium on Architectural Support for Programming Languages and Operating Systems; 1982:39–47 Palo Alto, CA, March.
[84 次回复]Thomas 代表作。《生成基于语言的环境》。马萨诸塞州剑桥:麻省理工学院出版社;1984 年,荣获 1983 年 ACM 博士论文奖。
[Rep84] Reps Thomas. Generating Language-Based Environments. Cambridge, MA: MIT Press; 1984 Winner of the 1983 ACM Doctoral Dissertation Award.
[RF93]Ramakrishna Rau B.,Fisher Joseph A。指令级并行处理:历史、概述和观点。《超级计算杂志》。1993;7(1/2):5 月 9-50 日。
[RF93] Ramakrishna Rau B., Fisher Joseph A. Instruction-level parallel processing: History, overview, and perspective. Journal of Supercomputing. 1993;7(1/2):9–50 May.
[Rob65]Robinson John Alan。基于归结原理的面向机器的逻辑。ACM杂志。1965;12(1):1 月 23-41 日。
[Rob65] Robinson John Alan. A machine-oriented logic based on the resolution principle. Journal of the ACM. 1965;12(1):23–41 January.
[Rob83]Robinson John Alan。逻辑编程——过去、现在和未来。新一代计算。1983;1(2):107–124。
[Rob83] Robinson John Alan. Logic programming—Past, present, and future. New Generation Computing. 1983;1(2):107–124.
[RR64]Randell Brian、Russell Lawford J. 编辑。《ALGOL 60 实现:计算机上 ALGOL 60 程序的翻译和使用》。纽约:Academic Press;1964 APIC 数据处理研究 #5。
[RR64] Randell Brian, Russell Lawford J., eds. ALGOL 60 Implementation: The Translation and Use of ALGOL 60 Programs on a Computer. New York, NY: Academic Press; 1964 A.P.I.C. Studies in Data Processing #5.
[RS59]Rabin Michael O.,Scott Dana S. 有限自动机及其决策问题。IBM研究与开发杂志。1959;3(2):114–125。
[RS59] Rabin Michael O., Scott Dana S. Finite automata and their decision problems. IBM Journal of Research and Development. 1959;3(2):114–125.
[RS70]Rosenkrantz Daniel J.,Stearns Richard E. 确定性自上而下语法的属性。信息与控制。1970;17(3):226-256 十月。
[RS70] Rosenkrantz Daniel J., Stearns Richard E. Properties of deterministic top-down grammars. Information and Control. 1970;17(3):226–256 October.
[RT88]Reps Thomas、Teitelbaum Timothy。合成器生成器:一种基于语言的编辑器构建系统。纽约:Springer-Verlag;1988 年。
[RT88] Reps Thomas, Teitelbaum Timothy. The Synthesizer Generator: A System for Constructing Language-Based Editors. New York, NY: Springer-Verlag; 1988.
[Rub87]Rubin Frank。“GOTO 被认为有害”被认为有害。ACM通讯。1987;30(3):195-196 年 3 月 进一步的通信见第 30 卷第 6、7、8、11 和 12 期。
[Rub87] Rubin Frank. ‘GOTO considered harmful’ considered harmful. Communications of the ACM. 1987;30(3):195–196 March Further correspondence appears in Volume 30, Numbers 6, 7, 8, 11, and 12.
[Rut67]鲁蒂豪瑟·海因茨。ALGOL 60 的描述。纽约,纽约:Springer-Verlag; 1967 年。
[Rut67] Rutishauser Heinz. Description of ALGOL 60. New York, NY: Springer-Verlag; 1967.
[RW92]Reiser Martin、Wirth Niklaus。《Oberon 编程——超越 Pascal 和 Modula》。马萨诸塞州雷丁:Addison-Wesley;1992 年。
[RW92] Reiser Martin, Wirth Niklaus. Programming in Oberon—Steps Beyond Pascal and Modula. Reading, MA: Addison-Wesley; 1992.
[SBG + 91]Strom Robert E.、Bacon David F.、Goldberg Arthur P.、Lowry Andy、Yellin Daniel M. 和 Yem Shaula Alexander 合著。Hermes :一种分布式计算语言。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1991 年。
[SBG+91] Strom Robert E., Bacon David F., Goldberg Arthur P., Lowry Andy, Yellin Daniel M., Yem Shaula Alexander, ini. Hermes: A Language for Distributed Computing. Englewood Cliffs, NJ: Prentice-Hall; 1991.
[SBN82]Siewiorek Daniel P.、Gordon Bell C.、Newell Allen。计算机结构:原理和示例。纽约:McGraw-Hill;1982 年。
[SBN82] Siewiorek Daniel P., Gordon Bell C., Newell Allen. Computer Structures: Principles and Examples. New York, NY: McGraw-Hill; 1982.
[SCG + 13]Shinn Alex、Cowan John、Gleckler Arthur A.、Ganz Steven、Hsu Aaron W.、Lucier Bradley、Medernach Emmanuel、Radul Alexey、Read Jeffrey T.、Rush David、Russel Benjamin L.、Shivers Olin、Snell-Pym Alaric、Sussman Gerald J.、Kelsey Richart、Clinger William、Rees Jonathan、Sperber Michael、Kent Dybvig R.、Flatt Matthew、Stratten Anton van。《算法语言方案报告》第 7 次修订版。2013 年 7 月,由 Shinn、Cowan 和 Gleckler 编辑。可在trac.sacrideo.us/wg/wiki/R7RSHomePage/上找到。
[SCG+13] Shinn Alex, Cowan John, Gleckler Arthur A., Ganz Steven, Hsu Aaron W., Lucier Bradley, Medernach Emmanuel, Radul Alexey, Read Jeffrey T., Rush David, Russel Benjamin L., Shivers Olin, Snell-Pym Alaric, Sussman Gerald J., Kelsey Richart, Clinger William, Rees Jonathan, Sperber Michael, Kent Dybvig R., Flatt Matthew, Stratten Anton van. Revised7 Report on the Algorithmic Language Scheme. 2013. July Edited by Shinn, Cowan, and Gleckler. Available at trac.sacrideo.us/wg/wiki/R7RSHomePage/.
[Sch03]Scholz Sven-Bodo。单一赋值 C——在函数式设置中高效支持高级数组操作。《函数式编程杂志》。2003;13(6):1005–1059。
[Sch03] Scholz Sven-Bodo. Single assignment C—Efficient support for high-level array operations in a functional setting. Journal of Functional Programming. 2003;13(6):1005–1059.
[SCK + 93]Sites Richard L., Chernoff Anton, Kirk Matthew B., Marks Maurice P., Robinson Scott G. 二进制翻译。ACM通讯。1993;36(2):2 月 69-81 日。
[SCK+93] Sites Richard L., Chernoff Anton, Kirk Matthew B., Marks Maurice P., Robinson Scott G. Binary translation. Communications of the ACM. 1993;36(2):69–81 February.
[Sco91]Scott Michael L. Lynx 分布式编程语言:动机、设计和体验。计算机语言。1991;16(3/4):209–233。
[Sco91] Scott Michael L. The Lynx distributed programming language: Motivation, design, and experience. Computer Languages. 1991;16(3/4):209–233.
[Sco13]Scott Michael L.共享内存同步。Morgan & Claypool 出版社;2013 年 6 月计算机架构综合讲座。
[Sco13] Scott Michael L. Shared-Memory Synchronization. Morgan & Claypool Publishers; 2013 Synthesis Lectures on Computer Architecture June.
[SDB84]Schwartz Mayer D.、Delisle Norman M.、Begwani Vimal S. Magpie 中的增量编译。摘自:SIGPLAN '84 编译器构建研讨会论文集;1984:122–131 加拿大魁北克省蒙特利尔,六月。
[SDB84] Schwartz Mayer D., Delisle Norman M., Begwani Vimal S. Incremental compilation in Magpie. In: Proceedings of the SIGPLAN '84 Symposium on Compiler Construction; 1984:122–131 Montreal, Quebec, Canada, June.
[SDDS86]Schwartz Jacob T.、Dewar Robert BK、Dubinsky Ed、Schonberg Edmond。《集合编程:SETL 简介》。纽约:Springer-Verlag;1986 年《计算机科学文本和专著》。
[SDDS86] Schwartz Jacob T., Dewar Robert B.K., Dubinsky Ed, Schonberg Edmond. Programming with Sets: An Introduction to SETL. New York, NY: Springer-Verlag; 1986 Texts and Monographs in Computer Science.
[自卫队+ 07]Sperber Michael、Kent Dybvig R.、Matthew Flatt、Straaten Anton van、Kelsey Richard、Clinger William、Rees Jonathan、Finder Robert Bruce 和 Matthews Jacob。《算法语言方案报告》第 6 版修订版。2007年 9 月由 Sperber、Dybvig、Flatt 和 van Stratten 编辑。可在r6rs.org/上找到。
[SDF+07] Sperber Michael, Kent Dybvig R., Flatt Matthew, Straaten Anton van, Kelsey Richard, Clinger William, Rees Jonathan, Findler Robert Bruce, Matthews Jacob. Revised6 Report on the Algorithmic Language Scheme. 2007. September Edited by Sperber, Dybvig, Flatt, and van Stratten. Available at r6rs.org/.
[SE94]Srivastava Amitabh,Eustace Alan。ATOM :一种用于构建自定义程序分析工具的系统。收录于:SIGPLAN 1994 年编程语言设计和实现会议论文集;1994:196-205 佛罗里达州奥兰多,六月。
[SE94] Srivastava Amitabh, Eustace Alan. ATOM: A system for building customized program analysis tools. In: Proceedings of the SIGPLAN 1994 Conference on Programming Language Design and Implementation; 1994:196–205 Orlando, FL, June.
[Seb15]Sebesta Robert W. 《编程语言概念》。第 11 版,马萨诸塞州波士顿:Pearson/Addison-Wesley;2015 年。
[Seb15] Sebesta Robert W. Concepts of Programming Languages. eleventh edition Boston, MA: Pearson/Addison-Wesley; 2015.
[Sei05]Peter Seibel。实用 Common Lisp。Apress LP;2005 年。
[Sei05] Seibel Peter. Practical Common Lisp. Apress L. P.; 2005.
[第96集]Sethi Ravi。《编程语言:概念和构造》。第二版,马萨诸塞州雷丁:Addison-Wesley;1996 年。
[Set96] Sethi Ravi. Programming Languages: Concepts and Constructs. second edition Reading, MA: Addison-Wesley; 1996.
[SF80]Solomon Marvin H.,Finkel Raphael A. 关于枚举二叉树的注释。《ACM 杂志》。1980;27(1):1 月 3-5 日。
[SF80] Solomon Marvin H., Finkel Raphael A. A note on enumerating binary trees. Journal of the ACM. 1980;27(1):3–5 January.
[SF88]Scott Michael L.,Finkel Raphael A. 一种跨编译单元类型安全的简单机制。IEEE软件工程学报。1988;SE–14(8):8 月 1238–1239 日。
[SF88] Scott Michael L., Finkel Raphael A. A simple mechanism for type security across compilation units. IEEE Transactions on Software Engineering. 1988;SE–14(8):1238–1239 August.
[西弗吉尼亚足球超级联赛+ 94]Schoinas Ioannis、Falsafi Babak、Lebeck Alvin R.、Reinhardt Steven K.、Larus James R.、Wood David A.分布式共享内存的细粒度访问控制。《第六届编程语言和操作系统架构支持国际会议论文集》;1994 年 10 月,加利福尼亚州圣何塞,297-306。
[SFL+94] Schoinas Ioannis, Falsafi Babak, Lebeck Alvin R., Reinhardt Steven K., Larus James R., Wood David A. Fine-grain access control for distributed shared memory. In: Proceedings of the Sixth International Conference on Architectural Support for Programming Languages and Operating Systems; 1994:297–306 San Jose, CA, October.
[第 96 号国家标准]Scales Daniel J.、Gharachorloo Kourosh。Shasta :一种支持细粒度共享内存的低开销纯软件方法。收录于:第七届编程语言和操作系统架构支持国际会议论文集;1996:174–185 马萨诸塞州剑桥,十月。
[SG96] Scales Daniel J., Gharachorloo Kourosh. Shasta: A low overhead, software-only approach for supporting fine-grain shared memory. In: Proceedings of the Seventh International Conference on Architectural Support for Programming Languages and Operating Systems; 1996:174–185 Cambridge, MA, October.
[SGC13]Syme Don、Granicz Adam、Cisternino Antonio。Expert F# 3.0。加州伯克利:Apress;2013 年。
[SGC13] Syme Don, Granicz Adam, Cisternino Antonio. Expert F# 3.0. Berkeley, CA: Apress; 2013.
[SH92]Sajeev ASM,John Hurst A. χ中的编程持久性。IEEE计算机。1992;25(9):9 月 57-66 日。
[SH92] Sajeev A.S.M., John Hurst A. Programming persistence in χ. IEEE Computer. 1992;25(9):57–66 September.
[SHC96]Somogyi Zoltan、Henderson Fergus、Conway Thomas。《Mercury 的执行算法:一种高效的纯声明式逻辑编程语言》。《逻辑编程杂志》。1996;29(1-3):10 月至 12 月 17-64 日。
[SHC96] Somogyi Zoltan, Henderson Fergus, Conway Thomas. The execution algorithm of Mercury: An efficient purely declarative logic programming language. Journal of Logic Programming. 1996;29(1-3):17–64 October–December.
[Sie00]Siegel Jon。CORBA 3 基础与编程。纽约:John Wiley and Sons;2000 年。
[Sie00] Siegel Jon. CORBA 3 Fundamentals and Programming. New York, NY: John Wiley and Sons; 2000.
[Sip13]Sipser Michael。《计算理论简介》。第三版,马萨诸塞州波士顿:Cengage Learning;2013 年。
[Sip13] Sipser Michael. Introduction to the Theory of Computation. third edition Boston, MA: Cengage Learning; 2013.
[SIT72]站点 Richard L. Algol W 参考手册。斯坦福,加利福尼亚州:斯坦福大学计算机科学系;1972 年技术报告 STAN-CS-71-230 二月。
[Sit72] Sites Richard L. Algol W reference manual. Stanford, CA: Computer Science Department, Stanford University; 1972 Technical Report STAN-CS-71-230 February.
[SK95]Slonneger Kenneth,Kurtz Barry L.编程语言的形式语法和语义:基于实验室的方法。马萨诸塞州雷丁:Addison-Wesley;1995 年。
[SK95] Slonneger Kenneth, Kurtz Barry L. Formal Syntax and Semantics of Programming Languages: A Laboratory Based Approach. Reading, MA: Addison-Wesley; 1995.
[SMC91]Saltz Joel H.、Mirchandaney Avi、Crowley Kay。循环的运行时并行化和调度。IEEE计算机学报。1991;40(5):5 月 603-612 日。
[SMC91] Saltz Joel H., Mirchandaney Avi, Crowley Kay. Run-time parallelization and scheduling of loops. IEEE Transactions on Computers. 1991;40(5):603–612 May.
[SPSS08]Shirako Jun、Peixotto David、Sarkar Vivek、Scherer William N. III。Phasers :用于集体和点对点同步的统一无死锁构造。收录于:第二十二届国际超级计算会议论文集;2008 年:277–288 希腊科斯岛,六月。
[SPSS08] Shirako Jun, Peixotto David, Sarkar Vivek, Scherer William N. III. Phasers: A unified deadlock-free construct for collective and point-to-point synchronization. In: Proceedings of the Twenty-Second International Conference on Supercomputing; 2008:277–288 Island of Kos, Greece, June.
[SR13]Sahami Mehran、Roach Steve 编。《计算机科学课程 2013:计算机科学本科学位课程指南》。2013年。计算机课程联合工作组、计算机协会 (ACM) 和 IEEE 计算机学会,12 月,可访问acm.org/education/CS2013-final-report.pdf。
[SR13] Sahami Mehran, Roach Steve, eds. Computer Science Curricula 2013: Curriculum Guidelines for Undergraduate Degree Programs in Computer Science. 2013. Joint Task Force on Computing Curricula, Association for Computing Machinery (ACM) and the IEEE Computer Society, December Available as acm.org/education/CS2013-final-report.pdf.
[斯里兰卡95]Srinivasan Raj。RPC :远程过程调用协议规范第 2 版。1995 年。互联网征求意见稿 #1831,8 月,可在rfc-archive.org/getrfc.php?rfc=1831上找到。
[Sri95] Srinivasan Raj. RPC: Remote procedure call protocol specification version 2. 1995. Internet Request for Comments #1831, August Available at rfc-archive.org/getrfc.php?rfc=1831.
[SS71]Scott Dana S.、Strachey Christopher。《面向计算机语言的数学语义》。收录于:Fox Jerome 编。《计算机和自动机研讨会论文集》。纽约:布鲁克林理工学院出版社;1971 年:19-46 页。
[SS71] Scott Dana S., Strachey Christopher. Toward a mathematical semantics for computer language. In: Fox Jerome, ed. Proceedings, Symposium on Computers and Automata. New York, NY: Polytechnic Institute of Brooklyn Press; 1971:19–46.
[SSA13]Schkufza Eric、Sharma Rahul、Aiken Alex。随机超优化。刊于:第十八届编程语言和操作系统架构支持国际会议论文集;2013 年:305–316 德克萨斯州休斯顿,3 月。
[SSA13] Schkufza Eric, Sharma Rahul, Aiken Alex. Stochastic superoptimization. In: Proceedings of the Eighteenth International Conference on Architectural Support for Programming Languages and Operating Systems; 2013:305–316 Houston, TX, March.
[SSD13]Sutton Andrew、Stroustrup Bjarne、Reis Gabriel Dos。Concepts Lite:使用谓词约束模板。2013年 3 月,国际标准化组织概念工作组,文档编号 N3580。可从open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3580.pdf获取。
[SSD13] Sutton Andrew, Stroustrup Bjarne, Reis Gabriel Dos. Concepts Lite: Constraining Templates with Predicates. 2013. March Document number N3580, Concepts working group, International Organization for Standardization. Available as open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3580.pdf.
[Sta95]Stansifer Ryan D. 《编程语言研究》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1995 年。
[Sta95] Stansifer Ryan D. The Study of Programming Languages. Englewood Cliffs, NJ: Prentice-Hall; 1995.
[Ste90]Jr Guy L. Steele。Common Lisp—语言。第二版,马萨诸塞州贝德福德:Digital Press;1990 年。可从cs.cmu.edu/Groups/AI/html/cltl/cltl2.html获取。
[Ste90] Jr Guy L. Steele. Common Lisp—The Language. second edition Bedford, MA: Digital Press; 1990. Available at cs.cmu.edu/Groups/AI/html/cltl/cltl2.html.
[Sto77]Stoy Joseph E.指称语义:Scott-Strachey 编程语言语义学方法,第 1 卷。马萨诸塞州剑桥:麻省理工学院出版社;1977 年。
[Sto77] Stoy Joseph E. Denotational Semantics: The Scott-Strachey Approach to Programming Language Semantics, volume 1. Cambridge, MA: MIT Press; 1977.
[第13话]Stroustrup Bjarne。《C++编程语言》。第四版 Addison-Wesley Professional;2013 年第二版,1991 年。
[Str13] Stroustrup Bjarne. The C++Programming Language. fourth edition Addison-Wesley Professional; 2013 Second edition, 1991.
[太阳90]Sunderam Vaidyalingam S. PVM:并行分布式计算框架。并发性——实践与经验。1990;2(4):315-339 十二月。
[Sun90] Sunderam Vaidyalingam S. PVM: A framework for parallel distributed computing. Concurrency—Practice and Experience. 1990;2(4):315–339 December.
[Sun97]Sun Microsystems,加利福尼亚州山景城。JavaBeans。1997年8 月,版本 1.01-A。可从oracle.com/technetwork/articles/javaee/spec-136004.html获取。
[Sun97] Sun Microsystems, Mountain View, CA. JavaBeans. 1997. August Version 1.01-A. Available at oracle.com/technetwork/articles/javaee/spec-136004.html.
[太阳04]Sundell H.高效实用的非阻塞数据结构。瑞典哥德堡:查尔姆斯理工大学和哥德堡大学;2004 年。计算机科学系博士论文,网址为cs.chalmers.se/~tsigas/papers/Haakan-Thesis.pdf。
[Sun04] Sundell H. Efficient and Practical Non-Blocking Data Structures. Goteborg, Sweden: Chalmers University of Technology and Goteborg University; 2004. Ph. D. dissertation, Department of Computing Science Available as cs.chalmers.se/~tsigas/papers/Haakan-Thesis.pdf.
[星期日06]Sun Microsystems。Strongtalk :需要速度的 Smalltalk。2006年。strongtalk.org。
[Sun06] Sun Microsystems. Strongtalk: Smalltalk with a need for speed. 2006. strongtalk.org.
[SW67]Schorr Herbert,Waite William M. 一种高效的独立于机器的各种列表结构垃圾收集程序。ACM通讯。1967;10(8):8 月 501-506 日。
[SW67] Schorr Herbert, Waite William M. An efficient machine-independent procedure for garbage collection in various list structures. Communications of the ACM. 1967;10(8):501–506 August.
[SW94]Smith James E.、Weiss Shlomo。PowerPC 601 和 Alpha 21064:两个 RISC 的故事。IEEE计算机。1994;27(6):6 月 46-58 日。
[SW94] Smith James E., Weiss Shlomo. PowerPC 601 and Alpha 21064: A tale of two RISCs. IEEE Computer. 1994;27(6):46–58 June.
[SZBH86]Swinehart Daniel C.、Zellweger Polle T.、Beach Richard J.、Hagmann Robert B. 的结构视图Cedar 编程环境。ACM编程语言和系统事务。1986;8(4):10 月 419-490。
[SZBH86] Swinehart Daniel C., Zellweger Polle T., Beach Richard J., Hagmann Robert B. A structural view of the Cedar programming environment. ACM Transactions on Programming Languages and Systems. 1986;8(4):419–490 October.
[Tan78]Tanenbaum Andrew S. Pascal 与 ALGOL 68 的比较。计算机杂志。1978;21(4):316-323 十一月。
[Tan78] Tanenbaum Andrew S. A comparison of Pascal and ALGOL 68. The Computer Journal. 1978;21(4):316–323 November.
[TFH13]Thomas Dave、Fowler Chad、Hunt Andy。《Ruby 1.9 和 2.0 编程——实用程序员指南》。LLC,德克萨斯州达拉斯:实用程序员;2013 年。
[TFH13] Thomas Dave, Fowler Chad, Hunt Andy. Programming Ruby 1.9 & 2.0—The Pragmatic Programmers' Guide. LLC, Dallas, TX: The Pragmatic Programmers; 2013.
[Tho95]Thompson Tom。构建更好的虚拟 CPU。字节。1995;20(8):149-150 八月。
[Tho95] Thompson Tom. Building the better virtual CPU. Byte. 1995;20(8):149–150 August.
[Tic86]Tichy Walter F. 智能重新编译。ACM编程语言和系统事务。1986;8(3):7 月 273-291 日。
[Tic86] Tichy Walter F. Smart recompilation. ACM Transactions on Programming Languages and Systems. 1986;8(3):273–291 July.
[TM81]Teitelman Warren,Masinter Larry。Interlisp 编程环境。IEEE计算机。1981;14(4):4 月 25-33 日。
[TM81] Teitelman Warren, Masinter Larry. The Interlisp programming environment. IEEE Computer. 1981;14(4):25–33 April.
[TML13]Kevin Tatroe、Peter MacIntyre、Lerdorf Rasmus。《PHP 编程》。第三版 Sebastopol,CA:O'Reilly Media;2013 年。
[TML13] Tatroe Kevin, MacIntyre Peter, Lerdorf Rasmus. Programming PHP. third edition Sebastopol, CA: O'Reilly Media; 2013.
[TR81]Teitelbaum Timothy,Reps Thomas。康奈尔程序合成器:语法制导编程环境。ACM通讯。1981;24(9):563-573 九月。
[TR81] Teitelbaum Timothy, Reps Thomas. The Cornell Program Synthesizer: A syntax-directed programming environment. Communications of the ACM. 1981;24(9):563–573 September.
[Tre86]Kent Treiber R.系统编程:应对并行性。IBM Almaden 研究中心;1986 年技术报告 RJ 5118 四月。
[Tre86] Kent Treiber R. Systems programming: Coping with parallelism. IBM Almaden Research Center; 1986 Technical Report RJ 5118 April.
[Tur86]Turner David A. Miranda 概述。ACM SIGPLAN 通知。1986;21(12):158-166 十二月。
[Tur86] Turner David A. An overview of Miranda. ACM SIGPLAN Notices. 1986;21(12):158–166 December.
[TW12]Tanenbaum Andrew S.、Wetherall David J.计算机网络。第五版 Pearson Higher Education;2012 年。
[TW12] Tanenbaum Andrew S., Wetherall David J. Computer Networks. fifth edition Pearson Higher Education; 2012.
[TWGM07]Tabba Fuad、Wang Cong、Goodman James R.、Moir Mark。NZTM :非阻塞零间接事务内存。见:第二届 ACM SIGPLAN 事务计算研讨会,俄勒冈州波特兰;2007 年 8 月,网址为cs.rochester.edu/meetings/TRANSACT07/papers/tabba.pdf。
[TWGM07] Tabba Fuad, Wang Cong, Goodman James R., Moir Mark. NZTM: Nonblocking zero-indirection transactional memory. In: Second ACM SIGPLAN Workshop on Transactional Computing, Portland, OR; 2007. August Available as cs.rochester.edu/meetings/TRANSACT07/papers/tabba.pdf.
[Ull85]Ullman Jeffrey D. 数据库逻辑查询语言的实现。ACM数据库系统事务。1985;10(3):9 月 289-321 日。
[Ull85] Ullman Jeffrey D. Implementation of logical query languages for databases. ACM Transactions on Database Systems. 1985;10(3):289–321 September.
[美国91]Ungar David,Smith Randall B. SELF:简单的力量。Lisp和符号计算。1991;4(3):7 月 187-205。
[US91] Ungar David, Smith Randall B. SELF: The power of simplicity. Lisp and Symbolic Computation. 1991;4(3):187–205 July.
[UW08]Ullman Jeffrey D.、Widom Jennifer。《数据库系统入门课程》。第三版 Upper Saddle River,新泽西州:Pearson/Prentice-Hall;2008 年。
[UW08] Ullman Jeffrey D., Widom Jennifer. A First Course in Database Systems. third edition Upper Saddle River, NJ: Pearson/Prentice-Hall; 2008.
[vCZ + 03]Behren Rob von、Condit Jeremy、Zhou Feng、Necula George C.、Brewer Eric。Capriccio :互联网服务的可扩展线程。收录于:第十九届 ACM 操作系统原理研讨会论文集;2003 年:268–281 博尔顿码头(乔治湖),纽约州,十月。
[vCZ+03] Behren Rob von, Condit Jeremy, Zhou Feng, Necula George C., Brewer Eric. Capriccio: Scalable threads for internet services. In: Proceedings of the Nineteenth ACM Symposium on Operating Systems Principles; 2003:268–281 Bolton Landing (Lake George), NY, October.
[VF82]Virgilio Thomas R.,Finkel Raphael A. 绑定策略和范围规则是独立的。计算机语言。1982;7(2):61-67。
[VF82] Virgilio Thomas R., Finkel Raphael A. Binding strategies and scope rules are independent. Computer Languages. 1982;7(2):61–67.
[VF94]Veenstra Jack E.、Fowler Robert J. Mint:共享内存多处理器高效仿真的前端。收录于:第二届计算机和电信系统建模、分析和仿真国际研讨会论文集;1994:201–207 北卡罗来纳州达勒姆,1 月。
[VF94] Veenstra Jack E., Fowler Robert J. Mint: A front end for efficient simulation of shared-memory multiprocessors. In: Proceedings of the Second International Workshop on Modeling, Analysis and Simulation of Computer and Telecommunication Systems; 1994:201–207 Durham, NC, January.
[虚拟点数+ 75]van Wijngaarden A.、Mailloux BJ、Peck JE.L.、Koster CHA、Sintzoff M.、Lindsey CH、Meertens LGLT.、Fisker RG《算法语言 ALGOL 68 的修订报告》。《Acta Informatica》。1975;5(1–3):1–236 另见 ACM SIGPLAN Notices,12(5):1–70,1977 年 5 月。
[vMP+75] van Wijngaarden A., Mailloux B.J., Peck JE.L., Koster C.H.A., Sintzoff M., Lindsey C.H., Meertens L.G.LT., Fisker R.G. Revised report on the algorithmic language ALGOL 68. Acta Informatica. 1975;5(1–3):1–236 Also ACM SIGPLAN Notices, 12(5):1–70, May 1977.
[vRD11]Guido van Rossum、Fred L.Drake Jr 编。《Python 语言参考手册》(3.2 版)。英国布里斯托尔:Network Theory, Ltd.;2011 年。
[vRD11] Guido van Rossum, Fred L.Drake Jr, eds. The Python Language Reference Manual (version 3.2). Bristol, UK: Network Theory, Ltd.; 2011.
[Wad98a]Wadler Philip。《六个愤怒的人》。ACM SIGPLAN 通告。1998;33(2):2 月 25-30 日。注意:本期封面上的目录不正确。
[Wad98a] Wadler Philip. An angry half-dozen. ACM SIGPLAN Notices. 1998;33(2):25–30 February NB: table of contents on cover of issue is incorrect.
[Wad98b]Wadler Philip。为什么没有人使用函数式语言。ACM SIGPLAN 通知。1998;33(8):8 月 23-27 日。
[Wad98b] Wadler Philip. Why no one uses functional languages. ACM SIGPLAN Notices. 1998;33(8):23–27 August.
[Wat77]Watt David Anthony。词缀语法的解析问题。Acta Informatica。1977;8(1):1-20。
[Wat77] Watt David Anthony. The parsing problem for affix grammars. Acta Informatica. 1977;8(1):1–20.
[Web89]Webb Fred。Fortran故事 — 真正的独家新闻。提交给 alt.folklore.computers。1989年 Mark Brader 在 ACM RISKS在线论坛第 9 卷第 54 期(1989 年 12 月 12 日)中引用。
[Web89] Webb Fred. Fortran story—The real scoop. Submitted to alt.folklore.computers. 1989 Quoted by Mark Brader in the ACM RISKS on-line forum, volume 9, issue 54, December 12, 1989.
[Weg90]Peter Wegner。面向对象编程的概念和范例。OOPS Messenger。1990 ;1(1):7-87 八月OOPSLA '89主题演讲的扩展版本。
[Weg90] Wegner Peter. Concepts and paradigms of object-oriented programming. OOPS Messenger. 1990;1(1):7–87 August Expanded version of the keynote address from OOPSLA '89.
[湿78]Wettstein Horst。重新审视嵌套监控调用问题。ACM操作系统评论。1978;12(1):1 月 19-23 日。
[Wet78] Wettstein Horst. The problem of nested monitor calls revisited. ACM Operating Systems Review. 1978;12(1):19–23 January.
[Wex78]Wexelblat Richard L. 编辑,ACM SIGPLAN 编程语言历史 (HOPL) 会议论文集,ACM 专著系列,1981 年,加利福尼亚州洛杉矶;纽约州纽约:Academic Press;1978 年 6 月。
[Wex78] Wexelblat Richard L., ed. Proceedings of the ACM SIGPLAN History of Programming Languages (HOPL) Conference, ACM Monograph Series, 1981, Los Angeles, CA; New York, NY: Academic Press; 1978 June.
[WH66]Wirth Niklaus,Hoare Charles Antony Richard。对 ALGOL 开发的贡献。ACM通讯。1966;9(6):6 月 413-431。
[WH66] Wirth Niklaus, Hoare Charles Antony Richard. A contribution to the development of ALGOL. Communications of the ACM. 1966;9(6):413–431 June.
[Wil92a]Wilson Paul R.页面错误时的指针调换:在标准硬件上高效且兼容地支持巨大地址空间。摘自:《操作系统面向对象国际研讨会论文集》;1992:364–377,法国巴黎,9 月。
[Wil92a] Wilson Paul R. Pointer swizzling at page fault time: Efficiently and compatibly supporting huge address spaces on standard hardware. In: Proceedings of the International Workshop on Object Orientation in Operating Systems; 1992:364–377 Paris, France, September.
[Wil92b]Wilson Paul R.单处理器垃圾收集技术。引自:《国际内存管理研讨会论文集》 , 《计算机科学讲义》第 637 卷;德国柏林:Springer-Verlag;1992 年:1-42。研讨会于 1992 年 9 月在法国圣马洛举行。扩展版本可从ftp://ftp.cs.utexas.edu/pub/garbage/bigsurv.ps获取。
[Wil92b] Wilson Paul R. Uniprocessor garbage collection techniques. In: Proceedings of the International Workshop on Memory Management, volume 637 of Lecture Notes in Computer Science; Berlin, Germany: Springer-Verlag; 1992:1–42. Workshop held at St. Malo, France, September 1992. Expanded version available as ftp://ftp.cs.utexas.edu/pub/garbage/bigsurv.ps.
[Win93]Winskel Glynn。《编程语言的形式语义》。马萨诸塞州剑桥:麻省理工学院出版社;1993 年。
[Win93] Winskel Glynn. The Formal Semantics of Programming Languages. Cambridge, MA: MIT Press; 1993.
[Wir71]Wirth Niklaus。编程语言 Pascal。Acta Informatica。1971;1(1):35–63。
[Wir71] Wirth Niklaus. The programming language Pascal. Acta Informatica. 1971;1(1):35–63.
[Wir76]Wirth Niklaus。算法 + 数据结构 = 程序。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1976 年。
[Wir76] Wirth Niklaus. Algorithms + Data Structures = Programs. Englewood Cliffs, NJ: Prentice-Hall; 1976.
[Wir77a]Wirth Niklaus。Modula 的设计和实现。软件——实践与经验。1977;7(1):67–84 一月至二月。
[Wir77a] Wirth Niklaus. Design and implementation of Modula. Software—Practice and Experience. 1977;7(1):67–84 January–February.
[Wir77b]Wirth Niklaus。Modula:一种模块化多道程序设计语言。软件——实践与经验。1977;7(1):1 月至 2 月 3-35 日。
[Wir77b] Wirth Niklaus. Modula: A language for modular multiprogramming. Software—Practice and Experience. 1977;7(1):3–35 January–February.
[Wir80]Wirth Niklaus。模块:高级编程语言中的系统结构工具。收录于:Tobias Jeffrey M. 主编的《语言设计和编程方法论》 , 《计算机科学讲义》第 79 卷。西德柏林:Springer-Verlag;1980:1-24 1979 年 9 月在澳大利亚悉尼举行的研讨会论文集。
[Wir80] Wirth Niklaus. The module: A system structuring facility in high-level programming languages. In: Tobias Jeffrey M., ed. Language Design and Programming Methodology, volume 79 of Lecture Notes in Computer Science. Berlin, West Germany: Springer-Verlag; 1980:1–24 Proceedings of a symposium held at Sydney, Australia, September 1979.
[Wir85a]Wirth Niklaus。从编程语言设计到计算机构造。ACM通讯。1985;28(2):159–164 二月 1984 年图灵奖演讲。
[Wir85a] Wirth Niklaus. From programming language design to computer construction. Communications of the ACM. 1985;28(2):159–164 February The 1984 Turing Award lecture.
[Wir85b]Wirth Niklaus。《Modula-2 编程》。第三版,修订版纽约:Springer-Verlag;1985 计算机科学文本和专著。
[Wir85b] Wirth Niklaus. Programming in Modula-2. third, corrected edition New York, NY: Springer-Verlag; 1985 Texts and Monographs in Computer Science.
[Wir88a]Wirth Niklaus。从 Modula 到 Oberon。软件——实践与经验。1988;18(7):661–670 七月。
[Wir88a] Wirth Niklaus. From Modula to Oberon. Software—Practice and Experience. 1988;18(7):661–670 July.
[Wir88b]Wirth Niklaus。编程语言 Oberon。软件——实践与经验。1988;18(7):671-690 七月。
[Wir88b] Wirth Niklaus. The programming language Oberon. Software—Practice and Experience. 1988;18(7):671–690 July.
[Wir88c]Wirth Niklaus。类型扩展。ACM编程语言和系统汇刊。1988;10(2):204-214 四月 相关通信见第 13 卷第 4 期。
[Wir88c] Wirth Niklaus. Type extensions. ACM Transactions on Programming Languages and Systems. 1988;10(2):204–214 April Relevant correspondence appears in Volume 13, Number 4.
[Wir07] NiklausWirth。Modula-2 和 Oberon。在HOPL III 会议论文集[Ass07] 中,第 3-1 至 3-10 页。
[Wir07] NiklausWirth. Modula-2 and Oberon. In HOPL III Proceedings [Ass07], pages 3-1–3-10.
[WJ93]Wilson Paul R.,Johnstone Mark S.实时非复制垃圾收集。出处:OOPSLA '93 内存管理和垃圾收集研讨会,华盛顿特区;1993 年 9 月。
[WJ93] Wilson Paul R., Johnstone Mark S. Real-time non-copying garbage collection. In: OOPSLA '93 Workshop on Memory Management and Gargage Collection, Washington, DC; 1993 September.
[WJH03]Welch Brent B.、Jones Ken、Hobbs Jeffrey。《Tcl 和 Tk 实用编程》。第四版 Upper Saddle River,新泽西州:Prentice-Hall;2003 以前版本的样章可在 beedub.com/book/ 在线获取。
[WJH03] Welch Brent B., Jones Ken, Hobbs Jeffrey. Practical Programming in Tcl and Tk. fourth edition Upper Saddle River, NJ: Prentice-Hall; 2003 Sample chapters from previous editions available on-line at beedub.com/book/.
[WLAG93]Wahbe Robert、Lucco Steven、Anderson Thomas E.、Graham Susan L.高效的基于软件的故障隔离。收录于:第十四届 ACM 操作系统原理研讨会论文集;1993:203–216 北卡罗来纳州阿什维尔,十二月。
[WLAG93] Wahbe Robert, Lucco Steven, Anderson Thomas E., Graham Susan L. Efficient software-based fault isolation. In: Proceedings of the Fourteenth ACM Symposium on Operating Systems Principles; 1993:203–216 Asheville, NC, December.
[WM95]Wilhelm Reinhard,Maurer Dieter。《编译器设计》。英国沃金厄姆:Addison-Wesley;1995 年由 Stephen S. Wilson 从德文译出。
[WM95] Wilhelm Reinhard, Maurer Dieter. Compiler Design. Wokingham, England: Addison-Wesley; 1995 Translated from the German by Stephen S. Wilson.
[WMWM87]Walker Janet H.、Moon David A.、Weinreb Daniel L.、McMahon Mike。Symbolics Genera 编程环境。IEEE软件。1987;4(6):11 月 36-45 日。
[WMWM87] Walker Janet H., Moon David A., Weinreb Daniel L., McMahon Mike. The Symbolics Genera programming environment. IEEE Software. 1987;4(6):36–45 November.
[Wol96]Wolfe Michael。《并行计算的高性能编译器》。加利福尼亚州雷德伍德城:Addison-Wesley;1996 年。
[Wol96] Wolfe Michael. High Performance Compilers for Parallel Computing. Redwood City, CA: Addison-Wesley; 1996.
[Wor05]万维网联盟。万维网字符模型 1.0:基础知识。2005年 2 月,网址为w3.org/TR/charmod/。
[Wor05] World Wide Web Consortium. Character Model for the World Wide Web 1.0: Fundamentals. 2005. February Available at w3.org/TR/charmod/.
[Wor06a]万维网联盟。可扩展标记语言 (XML) 1.1。第二版 2006 年 9 月 可在w3.org/TR/xml11/上获取。
[Wor06a] World Wide Web Consortium. Extensible Markup Language (XML) 1.1. Second Edition 2006. September Available at w3.org/TR/xml11/.
[Wor06b]万维网联盟。可扩展样式表语言 (XSL) 版本 1.1。2006年 12 月,可在w3.org/TR/xsl/上获取。
[Wor06b] World Wide Web Consortium. Extensible Stylesheet Language (XSL) Version 1.1. 2006. December Available at w3.org/TR/xsl/.
[Wor07]万维网联盟。XML路径语言 (XPath) 2.0。2007年 1 月,可在w3.org/TR/xpath20/上获取。
[Wor07] World Wide Web Consortium. XML Path Language (XPath) 2.0. 2007. January Available at w3.org/TR/xpath20/.
[Wor12]万维网联盟。SOAP当前状态。2012年。w3.org /standards/techs /soap。
[Wor12] World Wide Web Consortium. SOAP current status. 2012. w3.org/standards/techs/soap.
[Wor14]万维网联盟。XSL转换 (XSLT) 版本 3.0。2014年 10 月,可在w3.org/TR/xslt-30/上获取。
[Wor14] World Wide Web Consortium. XSL Transformations (XSLT) Version 3.0. 2014. October Available at w3.org/TR/xslt-30/.
[世界卫生大会第 77 届会议]Welsh Jim、Sneeringer WJ、Hoare Charles Antony Richard。Pascal 中的歧义和不安全性。软件实践与经验。1977;7(6):685–696 十一月至十二月。
[WSH77] Welsh Jim, Sneeringer W.J., Hoare Charles Antony Richard. Ambiguities and insecurities in Pascal. Software—Practice and Experience. 1977;7(6):685–696 November–December.
[YA93]Yang Jae-Heon,Anderson James H.快速、可扩展的同步,只需最少的硬件支持(扩展摘要)。摘自:第十二届 ACM 分布式计算原理研讨会论文集;1993:171–182 伊萨卡,纽约,八月。
[YA93] Yang Jae-Heon, Anderson James H. Fast, scalable synchronization with minimal hardware support (extended abstract). In: Proceedings of the Twelfth Annual ACM Symposium on Principles ofDistributed Computing; 1993:171–182 Ithaca, NY, August.
[你67]Younger Daniel H. 时间n 3中上下文无关语言的识别和解析。信息与控制。1967;10(2):189–208 年 2 月。
[You67] Younger Daniel H. Recognition and parsing of context-free languages in time n3. Information and Control. 1967;10(2):189–208 February.
[YTEM04]Yang Junfeng、Twohey Paul、Engler Dawson、Musuvathi Madanlal。使用模型检查查找严重的文件系统错误。在:第六届 USENIX 操作系统设计和实现研讨会论文集;2004 年:273–288 旧金山,加利福尼亚州,十二月。
[YTEM04] Yang Junfeng, Twohey Paul, Engler Dawson, Musuvathi Madanlal. Using model checking to find serious file system errors. In: Proceedings of the Sixth USENIX Symposium on Operating Systems Design and Implementation; 2004:273–288 San Francisco, CA, December.
[Zho96]周能发。再论Prolog实现中的参数传递和控制堆栈管理。ACM编程语言和系统学报。1996 ;18(6):752–779 十一月。
[Zho96] Zhou Neng-Fa. Parameter passing and control stack management in Prolog implementation revisited. ACM Transactions on Programming Languages and Systems. 1996;18(6):752–779 November.
[ZRA + 08]Zhao Qin、Rabbah Rodric、Amarasinghe Saman、Rudolph Larry、Wong Weng-Fai。如何完成一百万个观察点:使用动态检测进行高效调试。在:第十七届国际编译器构建会议论文集;2008:147-162 匈牙利布达佩斯,3 月。
[ZRA+08] Zhao Qin, Rabbah Rodric, Amarasinghe Saman, Rudolph Larry, Wong Weng-Fai. How to do a million watchpoints: Efficient debugging using dynamic instrumentation. In: Proceedings of the Seventeenth International Conference on Compiler Construction; 2008:147–162 Budapest, Hungary, March.
在每个条目中,首先列出正文中的页面,然后列出配套网站上的页面。
In each entry, pages in the main text are listed first, followed by pages on the companion site.
“ff”标记表明内容将继续涵盖后续页面。
The “ff” designation indicates that coverage continues on following pages.
0-9 和符号
0-9, and symbols
一个
A
乙
B
碳
C
德
D
埃
E
F
F
格
G
赫
H
我
I
J
J
钾
K
大号
L
米
M
否
N
哦
O
磷
P
问
Q
R
R
年代
S
电视
T
乌
U
五
V
西
W
十
X
是
Y
是
Z